@taskless-app/swarm 0.4.7 → 0.4.10
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/dist/cli.js +1243 -992
- package/dist/overmind.js +2744 -2334
- package/package.json +1 -4
package/dist/cli.js
CHANGED
|
@@ -1,102 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { existsSync } from "fs";
|
|
12
|
-
import { resolve as resolve2 } from "path";
|
|
13
|
-
|
|
14
|
-
// src/config.ts
|
|
15
|
-
import { homedir } from "os";
|
|
16
|
-
import { basename, join, resolve } from "path";
|
|
17
|
-
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
18
11
|
|
|
19
12
|
// ../swarm-core/src/types.ts
|
|
20
|
-
var EVENT_KINDS = [
|
|
21
|
-
"spawned",
|
|
22
|
-
"step",
|
|
23
|
-
"blocked",
|
|
24
|
-
"answered",
|
|
25
|
-
"pr_opened",
|
|
26
|
-
"merged",
|
|
27
|
-
"done",
|
|
28
|
-
"failed",
|
|
29
|
-
"stalled",
|
|
30
|
-
"respawned",
|
|
31
|
-
// дрон умер, рой авто-оживил (respawn/--continue) — счётчик respawns
|
|
32
|
-
"paused",
|
|
33
|
-
// дрон замер: overmind недоступен (heartbeat устарел) — сохранился и вышел (TASWM-41)
|
|
34
|
-
"resumed",
|
|
35
|
-
// новый overmind разбудил замерший прогон (--continue/fresh) (TASWM-41)
|
|
36
|
-
"rescued",
|
|
37
|
-
// worktree мёртвого дрона с несохранённой работой сохранён (не снесён) — забрать руками
|
|
38
|
-
"conflict",
|
|
39
|
-
// review-PR конфликтует с main — рой вливает main / поднял резолвера / разрешил (merge-main-in)
|
|
40
|
-
"died",
|
|
41
|
-
// работающий дрон умер (dead pid) — семпл для скользящего окна детекции шторма (TASWM-23)
|
|
42
|
-
"storm_start",
|
|
43
|
-
// рой вошёл в штормовой режим — Anthropic лежит, пауза (глобальное событие, run_id NULL) (TASWM-23)
|
|
44
|
-
"storm_end",
|
|
45
|
-
// отбой шторма — Anthropic вернулся, рой поднимается (глобальное событие, run_id NULL) (TASWM-23)
|
|
46
|
-
"storm_dead",
|
|
47
|
-
// дрон заморожен штормом (worktree сохранён, respawn не тронут) (TASWM-23)
|
|
48
|
-
"storm_probe",
|
|
49
|
-
// запись health-probe шторма: {probe, ext} — троттлинг 60с + счётчик успехов (глобальное, run_id NULL) (TASWM-23)
|
|
50
|
-
"stopped",
|
|
51
|
-
// прогон остановлен оператором (с причиной)
|
|
52
|
-
"superseded",
|
|
53
|
-
// прогон переигран на другой вариант
|
|
54
|
-
"question",
|
|
55
|
-
// вопрос дрона оператору (прод: SwarmEvent kind=question, TASWM-74)
|
|
56
|
-
"answer",
|
|
57
|
-
// ответ оператора на вопрос дрона (прод: SwarmEvent kind=answer)
|
|
58
|
-
"ci_wake",
|
|
59
|
-
// review-PR покраснел (gh pr checks) — рой разбудил дрона со сжатым summary фейла (TASWM-102)
|
|
60
|
-
"ci_checked",
|
|
61
|
-
// опрос `gh pr checks` без побудки (не fail/gh недоступен) — только для cooldown, не веха (TASWM-102)
|
|
62
|
-
"anomaly",
|
|
63
|
-
// встроенный надзор overmind засёк аномалию прогона — дубль нотификации в инбокс SwarmEvent'ом (TASWM-194)
|
|
64
|
-
// Заметка дрона «замечено по пути» (TASWM-279): НЕблокирующий канал к оператору рядом с ask.
|
|
65
|
-
// ask останавливает дрона и ждёт ответа — дорого, и оправдано только на развилках «без ответа
|
|
66
|
-
// делать нечего». Находки вроде «в дизайн-системе нет иконки микрофона» раньше либо
|
|
67
|
-
// превращались в такой дорогой вопрос, либо тонули в отчёте о сдаче (дрон LAMPYRIS сдал
|
|
68
|
-
// заглушку «Мик» текстом на кнопке — уехало в прод). note уезжает оператору, дрон работает дальше.
|
|
69
|
-
"note"
|
|
70
|
-
];
|
|
71
|
-
var MILESTONE_KINDS = [
|
|
72
|
-
"spawned",
|
|
73
|
-
"pr_opened",
|
|
74
|
-
"merged",
|
|
75
|
-
"done",
|
|
76
|
-
"failed",
|
|
77
|
-
"stalled",
|
|
78
|
-
"conflict",
|
|
79
|
-
"ci_wake",
|
|
80
|
-
// CI-побудка (TASWM-102) — тоже реконсайл-решение, веха как conflict
|
|
81
|
-
"stopped",
|
|
82
|
-
"superseded",
|
|
83
|
-
"paused",
|
|
84
|
-
"resumed",
|
|
85
|
-
"storm_start",
|
|
86
|
-
"storm_end",
|
|
87
|
-
"storm_dead",
|
|
88
|
-
"anomaly",
|
|
89
|
-
// надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
|
|
90
|
-
"note"
|
|
91
|
-
// заметка дрона «замечено по пути» (TASWM-279) — веха, иначе находка тонет в ленте step'ов
|
|
92
|
-
];
|
|
93
|
-
var MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
|
|
94
13
|
function isMilestoneKind(kind) {
|
|
95
14
|
return MILESTONE_KIND_SET.has(kind);
|
|
96
15
|
}
|
|
97
16
|
function toRunKind(role) {
|
|
98
17
|
return role === "hero" || role === "reviewer" ? role : "drone";
|
|
99
18
|
}
|
|
19
|
+
var EVENT_KINDS, MILESTONE_KINDS, MILESTONE_KIND_SET;
|
|
20
|
+
var init_types = __esm({
|
|
21
|
+
"../swarm-core/src/types.ts"() {
|
|
22
|
+
"use strict";
|
|
23
|
+
EVENT_KINDS = [
|
|
24
|
+
"spawned",
|
|
25
|
+
"step",
|
|
26
|
+
"blocked",
|
|
27
|
+
"answered",
|
|
28
|
+
"pr_opened",
|
|
29
|
+
"merged",
|
|
30
|
+
"done",
|
|
31
|
+
"failed",
|
|
32
|
+
"stalled",
|
|
33
|
+
"respawned",
|
|
34
|
+
// дрон умер, рой авто-оживил (respawn/--continue) — счётчик respawns
|
|
35
|
+
"paused",
|
|
36
|
+
// дрон замер: overmind недоступен (heartbeat устарел) — сохранился и вышел (TASWM-41)
|
|
37
|
+
"resumed",
|
|
38
|
+
// новый overmind разбудил замерший прогон (--continue/fresh) (TASWM-41)
|
|
39
|
+
"rescued",
|
|
40
|
+
// worktree мёртвого дрона с несохранённой работой сохранён (не снесён) — забрать руками
|
|
41
|
+
"conflict",
|
|
42
|
+
// review-PR конфликтует с main — рой вливает main / поднял резолвера / разрешил (merge-main-in)
|
|
43
|
+
"died",
|
|
44
|
+
// работающий дрон умер (dead pid) — семпл для скользящего окна детекции шторма (TASWM-23)
|
|
45
|
+
"storm_start",
|
|
46
|
+
// рой вошёл в штормовой режим — Anthropic лежит, пауза (глобальное событие, run_id NULL) (TASWM-23)
|
|
47
|
+
"storm_end",
|
|
48
|
+
// отбой шторма — Anthropic вернулся, рой поднимается (глобальное событие, run_id NULL) (TASWM-23)
|
|
49
|
+
"storm_dead",
|
|
50
|
+
// дрон заморожен штормом (worktree сохранён, respawn не тронут) (TASWM-23)
|
|
51
|
+
"storm_probe",
|
|
52
|
+
// запись health-probe шторма: {probe, ext} — троттлинг 60с + счётчик успехов (глобальное, run_id NULL) (TASWM-23)
|
|
53
|
+
"stopped",
|
|
54
|
+
// прогон остановлен оператором (с причиной)
|
|
55
|
+
"superseded",
|
|
56
|
+
// прогон переигран на другой вариант
|
|
57
|
+
"question",
|
|
58
|
+
// вопрос дрона оператору (прод: SwarmEvent kind=question, TASWM-74)
|
|
59
|
+
"answer",
|
|
60
|
+
// ответ оператора на вопрос дрона (прод: SwarmEvent kind=answer)
|
|
61
|
+
"ci_wake",
|
|
62
|
+
// review-PR покраснел (gh pr checks) — рой разбудил дрона со сжатым summary фейла (TASWM-102)
|
|
63
|
+
"ci_checked",
|
|
64
|
+
// опрос `gh pr checks` без побудки (не fail/gh недоступен) — только для cooldown, не веха (TASWM-102)
|
|
65
|
+
"anomaly",
|
|
66
|
+
// встроенный надзор overmind засёк аномалию прогона — дубль нотификации в инбокс SwarmEvent'ом (TASWM-194)
|
|
67
|
+
// Заметка дрона «замечено по пути» (TASWM-279): НЕблокирующий канал к оператору рядом с ask.
|
|
68
|
+
// ask останавливает дрона и ждёт ответа — дорого, и оправдано только на развилках «без ответа
|
|
69
|
+
// делать нечего». Находки вроде «в дизайн-системе нет иконки микрофона» раньше либо
|
|
70
|
+
// превращались в такой дорогой вопрос, либо тонули в отчёте о сдаче (дрон LAMPYRIS сдал
|
|
71
|
+
// заглушку «Мик» текстом на кнопке — уехало в прод). note уезжает оператору, дрон работает дальше.
|
|
72
|
+
"note"
|
|
73
|
+
];
|
|
74
|
+
MILESTONE_KINDS = [
|
|
75
|
+
"spawned",
|
|
76
|
+
"pr_opened",
|
|
77
|
+
"merged",
|
|
78
|
+
"done",
|
|
79
|
+
"failed",
|
|
80
|
+
"stalled",
|
|
81
|
+
"conflict",
|
|
82
|
+
"ci_wake",
|
|
83
|
+
// CI-побудка (TASWM-102) — тоже реконсайл-решение, веха как conflict
|
|
84
|
+
"stopped",
|
|
85
|
+
"superseded",
|
|
86
|
+
"paused",
|
|
87
|
+
"resumed",
|
|
88
|
+
"storm_start",
|
|
89
|
+
"storm_end",
|
|
90
|
+
"storm_dead",
|
|
91
|
+
"anomaly",
|
|
92
|
+
// надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
|
|
93
|
+
"note"
|
|
94
|
+
// заметка дрона «замечено по пути» (TASWM-279) — веха, иначе находка тонет в ленте step'ов
|
|
95
|
+
];
|
|
96
|
+
MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
100
99
|
|
|
101
100
|
// ../swarm-core/src/timeline.ts
|
|
102
101
|
function resolveDepRef(ref, lk) {
|
|
@@ -265,29 +264,22 @@ function computeCounters(lanes) {
|
|
|
265
264
|
}
|
|
266
265
|
return c;
|
|
267
266
|
}
|
|
268
|
-
var DEFAULT_RUN_MS
|
|
267
|
+
var DEFAULT_RUN_MS;
|
|
268
|
+
var init_timeline = __esm({
|
|
269
|
+
"../swarm-core/src/timeline.ts"() {
|
|
270
|
+
"use strict";
|
|
271
|
+
DEFAULT_RUN_MS = 25 * 6e4;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ../swarm-core/src/reviewPhase.ts
|
|
276
|
+
var init_reviewPhase = __esm({
|
|
277
|
+
"../swarm-core/src/reviewPhase.ts"() {
|
|
278
|
+
"use strict";
|
|
279
|
+
}
|
|
280
|
+
});
|
|
269
281
|
|
|
270
282
|
// ../swarm-core/src/status.ts
|
|
271
|
-
var STATUS_META = {
|
|
272
|
-
queued: { pill: "queued", label: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438", color: "--muted-foreground", terminal: false },
|
|
273
|
-
running: { pill: "run", label: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", color: "--primary", terminal: false },
|
|
274
|
-
blocked: { pill: "blocked", label: "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430", color: "--warning", terminal: false },
|
|
275
|
-
paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning", terminal: false },
|
|
276
|
-
// overmind был недоступен — ждёт пробуждения (TASWM-41)
|
|
277
|
-
review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info", terminal: false },
|
|
278
|
-
done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success", terminal: true },
|
|
279
|
-
failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive", terminal: true },
|
|
280
|
-
stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive", terminal: false },
|
|
281
|
-
// молчит/погиб, но ещё разруливается — не терминал
|
|
282
|
-
storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning", terminal: false },
|
|
283
|
-
// умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
|
|
284
|
-
stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground", terminal: true },
|
|
285
|
-
superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true }
|
|
286
|
-
};
|
|
287
|
-
var FALLBACK = STATUS_META.queued;
|
|
288
|
-
var TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
|
|
289
|
-
(s) => STATUS_META[s].terminal
|
|
290
|
-
);
|
|
291
283
|
function statusMeta(status) {
|
|
292
284
|
return STATUS_META[status] ?? FALLBACK;
|
|
293
285
|
}
|
|
@@ -301,6 +293,32 @@ function statusTuples() {
|
|
|
301
293
|
}
|
|
302
294
|
return out;
|
|
303
295
|
}
|
|
296
|
+
var STATUS_META, FALLBACK, TERMINAL_STATUSES;
|
|
297
|
+
var init_status = __esm({
|
|
298
|
+
"../swarm-core/src/status.ts"() {
|
|
299
|
+
"use strict";
|
|
300
|
+
STATUS_META = {
|
|
301
|
+
queued: { pill: "queued", label: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438", color: "--muted-foreground", terminal: false },
|
|
302
|
+
running: { pill: "run", label: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", color: "--primary", terminal: false },
|
|
303
|
+
blocked: { pill: "blocked", label: "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430", color: "--warning", terminal: false },
|
|
304
|
+
paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning", terminal: false },
|
|
305
|
+
// overmind был недоступен — ждёт пробуждения (TASWM-41)
|
|
306
|
+
review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info", terminal: false },
|
|
307
|
+
done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success", terminal: true },
|
|
308
|
+
failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive", terminal: true },
|
|
309
|
+
stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive", terminal: false },
|
|
310
|
+
// молчит/погиб, но ещё разруливается — не терминал
|
|
311
|
+
storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning", terminal: false },
|
|
312
|
+
// умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
|
|
313
|
+
stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground", terminal: true },
|
|
314
|
+
superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true }
|
|
315
|
+
};
|
|
316
|
+
FALLBACK = STATUS_META.queued;
|
|
317
|
+
TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
|
|
318
|
+
(s) => STATUS_META[s].terminal
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
304
322
|
|
|
305
323
|
// ../swarm-core/src/time.ts
|
|
306
324
|
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
@@ -318,10 +336,15 @@ function toStamp(v) {
|
|
|
318
336
|
if (Number.isNaN(d.getTime())) return v.includes("T") ? v.slice(0, 19).replace("T", " ") : v;
|
|
319
337
|
return stamp(d);
|
|
320
338
|
}
|
|
321
|
-
var DAY_MS
|
|
339
|
+
var DAY_MS;
|
|
340
|
+
var init_time = __esm({
|
|
341
|
+
"../swarm-core/src/time.ts"() {
|
|
342
|
+
"use strict";
|
|
343
|
+
DAY_MS = 864e5;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
322
346
|
|
|
323
347
|
// ../swarm-core/src/windows.ts
|
|
324
|
-
var ROY_WINDOW_DEFAULTS = { windowH: 5, maxDone: 6, axisH: 6 };
|
|
325
348
|
function resolveRoyWindows(env = {}) {
|
|
326
349
|
return {
|
|
327
350
|
windowMs: Number(env.ROY_WINDOW_H || ROY_WINDOW_DEFAULTS.windowH) * 36e5,
|
|
@@ -329,46 +352,50 @@ function resolveRoyWindows(env = {}) {
|
|
|
329
352
|
maxAxisMs: Number(env.ROY_AXIS_H || ROY_WINDOW_DEFAULTS.axisH) * 36e5
|
|
330
353
|
};
|
|
331
354
|
}
|
|
355
|
+
var ROY_WINDOW_DEFAULTS;
|
|
356
|
+
var init_windows = __esm({
|
|
357
|
+
"../swarm-core/src/windows.ts"() {
|
|
358
|
+
"use strict";
|
|
359
|
+
ROY_WINDOW_DEFAULTS = { windowH: 5, maxDone: 6, axisH: 6 };
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ../swarm-core/src/slug.ts
|
|
364
|
+
var init_slug = __esm({
|
|
365
|
+
"../swarm-core/src/slug.ts"() {
|
|
366
|
+
"use strict";
|
|
367
|
+
}
|
|
368
|
+
});
|
|
332
369
|
|
|
333
370
|
// ../swarm-core/src/text.ts
|
|
334
|
-
var ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
335
|
-
var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
|
|
336
371
|
function sanitizeEventText(text) {
|
|
337
372
|
if (text == null) return null;
|
|
338
373
|
const cleaned = text.replace(ANSI_CSI_RE, "").replace(CONTROL_CHARS_RE, " ").replace(/ {2,}/g, " ").trim();
|
|
339
|
-
|
|
340
|
-
|
|
374
|
+
if (!cleaned.length) return null;
|
|
375
|
+
return cleaned.length <= EVENT_TEXT_MAX ? cleaned : clampEventText(cleaned);
|
|
376
|
+
}
|
|
377
|
+
function clampEventText(text) {
|
|
378
|
+
const note = ` \u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u0431\u044B\u043B\u043E ${text.length} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432)`;
|
|
379
|
+
return text.slice(0, Math.max(0, EVENT_TEXT_MAX - note.length)) + note;
|
|
380
|
+
}
|
|
381
|
+
var ANSI_CSI_RE, CONTROL_CHARS_RE, EVENT_TEXT_MAX;
|
|
382
|
+
var init_text = __esm({
|
|
383
|
+
"../swarm-core/src/text.ts"() {
|
|
384
|
+
"use strict";
|
|
385
|
+
ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
386
|
+
CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
|
|
387
|
+
EVENT_TEXT_MAX = 4e3;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ../swarm-core/src/comment.ts
|
|
392
|
+
var init_comment = __esm({
|
|
393
|
+
"../swarm-core/src/comment.ts"() {
|
|
394
|
+
"use strict";
|
|
395
|
+
}
|
|
396
|
+
});
|
|
341
397
|
|
|
342
398
|
// ../swarm-core/src/names.ts
|
|
343
|
-
var DRONE_POOL = [
|
|
344
|
-
// Латинский бестиарий (15)
|
|
345
|
-
"SCARAB",
|
|
346
|
-
"VESPA",
|
|
347
|
-
"MANTIS",
|
|
348
|
-
"HORNET",
|
|
349
|
-
"CICADA",
|
|
350
|
-
"LOCUST",
|
|
351
|
-
"ATTA",
|
|
352
|
-
"FORMICA",
|
|
353
|
-
"MYRMEX",
|
|
354
|
-
"APIS",
|
|
355
|
-
"TERMES",
|
|
356
|
-
"GRYLLUS",
|
|
357
|
-
"LAMPYRIS",
|
|
358
|
-
"ARACHNE",
|
|
359
|
-
"SPHINX",
|
|
360
|
-
// Мифо-стаи (9)
|
|
361
|
-
"TIAMAT",
|
|
362
|
-
"JORMUNGAND",
|
|
363
|
-
"GRENDEL",
|
|
364
|
-
"TYPHON",
|
|
365
|
-
"FENRIS",
|
|
366
|
-
"GARM",
|
|
367
|
-
"KUKULKAN",
|
|
368
|
-
"LEVIATHAN",
|
|
369
|
-
"SURTUR"
|
|
370
|
-
];
|
|
371
|
-
var ACTIVE_NAME_STATUSES = ["queued", "running", "blocked", "review"];
|
|
372
399
|
function toRoman(n) {
|
|
373
400
|
if (n < 1 || n > 3999) return String(n);
|
|
374
401
|
const table = [
|
|
@@ -396,10 +423,43 @@ function genName(base3, g) {
|
|
|
396
423
|
function baseName(name) {
|
|
397
424
|
return name.split(" ")[0];
|
|
398
425
|
}
|
|
426
|
+
var DRONE_POOL, ACTIVE_NAME_STATUSES;
|
|
427
|
+
var init_names = __esm({
|
|
428
|
+
"../swarm-core/src/names.ts"() {
|
|
429
|
+
"use strict";
|
|
430
|
+
DRONE_POOL = [
|
|
431
|
+
// Латинский бестиарий (15)
|
|
432
|
+
"SCARAB",
|
|
433
|
+
"VESPA",
|
|
434
|
+
"MANTIS",
|
|
435
|
+
"HORNET",
|
|
436
|
+
"CICADA",
|
|
437
|
+
"LOCUST",
|
|
438
|
+
"ATTA",
|
|
439
|
+
"FORMICA",
|
|
440
|
+
"MYRMEX",
|
|
441
|
+
"APIS",
|
|
442
|
+
"TERMES",
|
|
443
|
+
"GRYLLUS",
|
|
444
|
+
"LAMPYRIS",
|
|
445
|
+
"ARACHNE",
|
|
446
|
+
"SPHINX",
|
|
447
|
+
// Мифо-стаи (9)
|
|
448
|
+
"TIAMAT",
|
|
449
|
+
"JORMUNGAND",
|
|
450
|
+
"GRENDEL",
|
|
451
|
+
"TYPHON",
|
|
452
|
+
"FENRIS",
|
|
453
|
+
"GARM",
|
|
454
|
+
"KUKULKAN",
|
|
455
|
+
"LEVIATHAN",
|
|
456
|
+
"SURTUR"
|
|
457
|
+
];
|
|
458
|
+
ACTIVE_NAME_STATUSES = ["queued", "running", "blocked", "review"];
|
|
459
|
+
}
|
|
460
|
+
});
|
|
399
461
|
|
|
400
462
|
// ../swarm-core/src/baseBranch.ts
|
|
401
|
-
var BASE_BRANCH_MAX = 255;
|
|
402
|
-
var BASE_BRANCH_RE = /^[A-Za-z0-9._][A-Za-z0-9._/-]*$/;
|
|
403
463
|
function normalizeBaseBranch(raw) {
|
|
404
464
|
if (raw == null) return void 0;
|
|
405
465
|
if (typeof raw !== "string") return null;
|
|
@@ -409,19 +469,22 @@ function normalizeBaseBranch(raw) {
|
|
|
409
469
|
if (v.includes("..") || v.endsWith("/") || v.endsWith(".lock")) return null;
|
|
410
470
|
return BASE_BRANCH_RE.test(v) ? v : null;
|
|
411
471
|
}
|
|
472
|
+
var BASE_BRANCH_MAX, BASE_BRANCH_RE;
|
|
473
|
+
var init_baseBranch = __esm({
|
|
474
|
+
"../swarm-core/src/baseBranch.ts"() {
|
|
475
|
+
"use strict";
|
|
476
|
+
BASE_BRANCH_MAX = 255;
|
|
477
|
+
BASE_BRANCH_RE = /^[A-Za-z0-9._][A-Za-z0-9._/-]*$/;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
412
480
|
|
|
413
481
|
// ../swarm-core/src/hosts.ts
|
|
414
|
-
var ANY_HOST_TOKENS = /* @__PURE__ */ new Set(["any", "none", "null", "free", "-", "\u043B\u044E\u0431\u0430\u044F", "\u043B\u044E\u0431\u043E\u0439"]);
|
|
415
482
|
function normalizeHostRef(value) {
|
|
416
483
|
if (value == null) return null;
|
|
417
484
|
const trimmed = value.trim();
|
|
418
485
|
if (!trimmed) return null;
|
|
419
486
|
return ANY_HOST_TOKENS.has(trimmed.toLowerCase()) ? null : trimmed;
|
|
420
487
|
}
|
|
421
|
-
var UNKNOWN_HOST_KEY = "__no-host__";
|
|
422
|
-
var UNKNOWN_HOST_LABEL = "\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430";
|
|
423
|
-
var QUEUE_LABEL = "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438";
|
|
424
|
-
var NOT_STARTED_STATUSES = /* @__PURE__ */ new Set(["queued"]);
|
|
425
488
|
function splitQueue(items, ref) {
|
|
426
489
|
const queue = [];
|
|
427
490
|
const hosted = [];
|
|
@@ -432,7 +495,6 @@ function splitQueue(items, ref) {
|
|
|
432
495
|
}
|
|
433
496
|
return { queue, hosted };
|
|
434
497
|
}
|
|
435
|
-
var HOST_HEARTBEAT_STALE_MS = 45e3;
|
|
436
498
|
function groupByHost(items, ref, opts = {}) {
|
|
437
499
|
const { hosts = [], nowMs = Date.now(), staleMs = HOST_HEARTBEAT_STALE_MS } = opts;
|
|
438
500
|
const infoById = new Map(hosts.map((h) => [h.hostId, h]));
|
|
@@ -488,10 +550,65 @@ function hostStateText(host, nowMs = Date.now(), staleMs = HOST_HEARTBEAT_STALE_
|
|
|
488
550
|
function shouldGroupByHost(groups) {
|
|
489
551
|
return groups.length > 1;
|
|
490
552
|
}
|
|
553
|
+
var ANY_HOST_TOKENS, UNKNOWN_HOST_KEY, UNKNOWN_HOST_LABEL, QUEUE_LABEL, NOT_STARTED_STATUSES, HOST_HEARTBEAT_STALE_MS;
|
|
554
|
+
var init_hosts = __esm({
|
|
555
|
+
"../swarm-core/src/hosts.ts"() {
|
|
556
|
+
"use strict";
|
|
557
|
+
ANY_HOST_TOKENS = /* @__PURE__ */ new Set(["any", "none", "null", "free", "-", "\u043B\u044E\u0431\u0430\u044F", "\u043B\u044E\u0431\u043E\u0439"]);
|
|
558
|
+
UNKNOWN_HOST_KEY = "__no-host__";
|
|
559
|
+
UNKNOWN_HOST_LABEL = "\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430";
|
|
560
|
+
QUEUE_LABEL = "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438";
|
|
561
|
+
NOT_STARTED_STATUSES = /* @__PURE__ */ new Set(["queued"]);
|
|
562
|
+
HOST_HEARTBEAT_STALE_MS = 45e3;
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// ../swarm-core/src/hostCommands.ts
|
|
567
|
+
var HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS;
|
|
568
|
+
var init_hostCommands = __esm({
|
|
569
|
+
"../swarm-core/src/hostCommands.ts"() {
|
|
570
|
+
"use strict";
|
|
571
|
+
HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
|
|
572
|
+
TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
|
|
573
|
+
HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
|
|
574
|
+
HOST_COMMAND_TTL_MS = 30 * 6e4;
|
|
575
|
+
HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// ../swarm-core/src/relief.ts
|
|
580
|
+
var init_relief = __esm({
|
|
581
|
+
"../swarm-core/src/relief.ts"() {
|
|
582
|
+
"use strict";
|
|
583
|
+
init_time();
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ../swarm-core/src/index.ts
|
|
588
|
+
var init_src = __esm({
|
|
589
|
+
"../swarm-core/src/index.ts"() {
|
|
590
|
+
"use strict";
|
|
591
|
+
init_types();
|
|
592
|
+
init_timeline();
|
|
593
|
+
init_reviewPhase();
|
|
594
|
+
init_status();
|
|
595
|
+
init_time();
|
|
596
|
+
init_windows();
|
|
597
|
+
init_slug();
|
|
598
|
+
init_text();
|
|
599
|
+
init_comment();
|
|
600
|
+
init_names();
|
|
601
|
+
init_baseBranch();
|
|
602
|
+
init_hosts();
|
|
603
|
+
init_hostCommands();
|
|
604
|
+
init_relief();
|
|
605
|
+
}
|
|
606
|
+
});
|
|
491
607
|
|
|
492
608
|
// src/config.ts
|
|
493
|
-
|
|
494
|
-
|
|
609
|
+
import { homedir } from "os";
|
|
610
|
+
import { basename, join, resolve } from "path";
|
|
611
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
|
|
495
612
|
function normalizeRepo(repo) {
|
|
496
613
|
const r = (repo ?? "").trim();
|
|
497
614
|
if (!r) return "";
|
|
@@ -501,15 +618,6 @@ function normalizeRepo(repo) {
|
|
|
501
618
|
return resolve(r);
|
|
502
619
|
}
|
|
503
620
|
}
|
|
504
|
-
var DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
|
|
505
|
-
var DRONE_HOME = process.env.SWARM_DRONE_HOME || join(SWARM_HOME, "drone-home");
|
|
506
|
-
var DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
507
|
-
var DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
508
|
-
var WORKTREES_NOINDEX_DIR = join(SWARM_HOME, "worktrees.noindex");
|
|
509
|
-
var WORKTREES_LEGACY_DIR = join(SWARM_HOME, "worktrees");
|
|
510
|
-
var WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
511
|
-
var RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
|
|
512
|
-
var DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join(SWARM_HOME, "overmind.log");
|
|
513
621
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
514
622
|
return join(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
515
623
|
}
|
|
@@ -519,49 +627,12 @@ function resolveEffectiveWorktreesDir(deps) {
|
|
|
519
627
|
if (deps.liveDronesUnder(deps.legacyDir)) return deps.legacyDir;
|
|
520
628
|
return deps.targetDir;
|
|
521
629
|
}
|
|
522
|
-
var DIST_BUILD = true;
|
|
523
|
-
var SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
524
|
-
var SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
|
|
525
630
|
function envNum(name, def) {
|
|
526
631
|
const raw = process.env[name];
|
|
527
632
|
if (raw === void 0 || raw === "") return def;
|
|
528
633
|
const n = Number(raw);
|
|
529
634
|
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
530
635
|
}
|
|
531
|
-
var SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
532
|
-
var SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
533
|
-
var SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
534
|
-
var SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
535
|
-
var SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
536
|
-
var SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
|
|
537
|
-
var SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
|
|
538
|
-
var SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
|
|
539
|
-
var SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
|
|
540
|
-
var SWARM_RELIEF_CPU_BUSY = envNum("SWARM_RELIEF_CPU_BUSY", 0.95);
|
|
541
|
-
var SWARM_RELIEF_COST_WINDOW_MS = envNum("SWARM_RELIEF_COST_WINDOW_MS", 60 * 6e4);
|
|
542
|
-
var SWARM_RELIEF_ALERT_PAUSES = Math.trunc(envNum("SWARM_RELIEF_ALERT_PAUSES", 6));
|
|
543
|
-
var SWARM_RELIEF_ALERT_LOST_MS = envNum("SWARM_RELIEF_ALERT_LOST_MS", 10 * 6e4);
|
|
544
|
-
var SWARM_RELIEF_ALERT_MIN_STEPS = envNum("SWARM_RELIEF_ALERT_MIN_STEPS", 1);
|
|
545
|
-
var SWARM_RELIEF_ALERT_SUSTAIN_MS = envNum("SWARM_RELIEF_ALERT_SUSTAIN_MS", 10 * 6e4);
|
|
546
|
-
var SWARM_LOW_DISK_GB = envNum("SWARM_LOW_DISK_GB", 10);
|
|
547
|
-
var SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
548
|
-
var SWARM_DRONE_BRIDGE_MS = envNum("SWARM_DRONE_BRIDGE_MS", 5e3);
|
|
549
|
-
var SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
550
|
-
var SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
|
|
551
|
-
var SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
552
|
-
var DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
553
|
-
var DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
554
|
-
var DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
555
|
-
var DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join(SWARM_HOME, "drone-channel.key");
|
|
556
|
-
var DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
557
|
-
var HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
558
|
-
var HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
559
|
-
var SWARM_REPO = process.env.SWARM_REPO || "";
|
|
560
|
-
var SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
561
|
-
var SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
562
|
-
var TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
563
|
-
var SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
564
|
-
var TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
565
636
|
function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
566
637
|
const base3 = api.replace(/\/$/, "");
|
|
567
638
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -574,32 +645,86 @@ function tokenFromFile() {
|
|
|
574
645
|
return "";
|
|
575
646
|
}
|
|
576
647
|
}
|
|
577
|
-
var
|
|
578
|
-
var
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
648
|
+
var SWARM_HOME, DB_PATH, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
|
|
649
|
+
var init_config = __esm({
|
|
650
|
+
"src/config.ts"() {
|
|
651
|
+
"use strict";
|
|
652
|
+
init_src();
|
|
653
|
+
SWARM_HOME = process.env.SWARM_HOME || join(homedir(), ".taskless-swarm");
|
|
654
|
+
mkdirSync(SWARM_HOME, { recursive: true });
|
|
655
|
+
DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
|
|
656
|
+
DRONE_HOME = process.env.SWARM_DRONE_HOME || join(SWARM_HOME, "drone-home");
|
|
657
|
+
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
658
|
+
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
659
|
+
WORKTREES_NOINDEX_DIR = join(SWARM_HOME, "worktrees.noindex");
|
|
660
|
+
WORKTREES_LEGACY_DIR = join(SWARM_HOME, "worktrees");
|
|
661
|
+
WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
662
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
|
|
663
|
+
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join(SWARM_HOME, "overmind.log");
|
|
664
|
+
DIST_BUILD = true;
|
|
665
|
+
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
666
|
+
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
|
|
667
|
+
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
668
|
+
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
669
|
+
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
670
|
+
SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
671
|
+
SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
672
|
+
SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
|
|
673
|
+
SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
|
|
674
|
+
SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
|
|
675
|
+
SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
|
|
676
|
+
SWARM_RELIEF_CPU_BUSY = envNum("SWARM_RELIEF_CPU_BUSY", 0.95);
|
|
677
|
+
SWARM_RELIEF_COST_WINDOW_MS = envNum("SWARM_RELIEF_COST_WINDOW_MS", 60 * 6e4);
|
|
678
|
+
SWARM_RELIEF_ALERT_PAUSES = Math.trunc(envNum("SWARM_RELIEF_ALERT_PAUSES", 6));
|
|
679
|
+
SWARM_RELIEF_ALERT_LOST_MS = envNum("SWARM_RELIEF_ALERT_LOST_MS", 10 * 6e4);
|
|
680
|
+
SWARM_RELIEF_ALERT_MIN_STEPS = envNum("SWARM_RELIEF_ALERT_MIN_STEPS", 1);
|
|
681
|
+
SWARM_RELIEF_ALERT_SUSTAIN_MS = envNum("SWARM_RELIEF_ALERT_SUSTAIN_MS", 10 * 6e4);
|
|
682
|
+
SWARM_LOW_DISK_GB = envNum("SWARM_LOW_DISK_GB", 10);
|
|
683
|
+
SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
684
|
+
SWARM_DRONE_BRIDGE_MS = envNum("SWARM_DRONE_BRIDGE_MS", 5e3);
|
|
685
|
+
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
686
|
+
SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
|
|
687
|
+
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
688
|
+
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
689
|
+
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
690
|
+
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
691
|
+
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join(SWARM_HOME, "drone-channel.key");
|
|
692
|
+
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
693
|
+
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
694
|
+
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
695
|
+
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
696
|
+
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
697
|
+
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
698
|
+
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
699
|
+
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
700
|
+
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
701
|
+
TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
|
|
702
|
+
TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
|
|
703
|
+
TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
|
|
704
|
+
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
705
|
+
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
706
|
+
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
707
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
|
|
708
|
+
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
709
|
+
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
710
|
+
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
711
|
+
WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
712
|
+
SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
|
|
713
|
+
SWARM_ANOMALY_MAX_REPEATS = Math.trunc(envNum("SWARM_ANOMALY_MAX_REPEATS", 3));
|
|
714
|
+
SWARM_ANOMALY_RUN_DOWN_WINDOW_MS = envNum("SWARM_ANOMALY_RUN_DOWN_WINDOW_MS", 24 * 60 * 6e4);
|
|
715
|
+
SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
|
|
716
|
+
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
|
|
717
|
+
SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
|
|
718
|
+
SWARM_ANOMALY_PR_STUCK_MERGE_MS = envNum("SWARM_ANOMALY_PR_STUCK_MERGE_MS", 30 * 6e4);
|
|
719
|
+
SWARM_ANOMALY_PR_NO_REVIEWER_MS = envNum("SWARM_ANOMALY_PR_NO_REVIEWER_MS", 20 * 6e4);
|
|
720
|
+
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
721
|
+
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
722
|
+
SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
|
|
723
|
+
SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
600
726
|
|
|
601
727
|
// src/names.ts
|
|
602
|
-
var NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
|
|
603
728
|
function pickName(rows) {
|
|
604
729
|
const live = /* @__PURE__ */ new Set();
|
|
605
730
|
const lastUse = /* @__PURE__ */ new Map();
|
|
@@ -619,21 +744,16 @@ function pickName(rows) {
|
|
|
619
744
|
}
|
|
620
745
|
}
|
|
621
746
|
}
|
|
747
|
+
var NAME_LIVE;
|
|
748
|
+
var init_names2 = __esm({
|
|
749
|
+
"src/names.ts"() {
|
|
750
|
+
"use strict";
|
|
751
|
+
init_src();
|
|
752
|
+
NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
622
755
|
|
|
623
756
|
// src/storm.ts
|
|
624
|
-
var numEnv = (name, def) => {
|
|
625
|
-
const v = Number(process.env[name]);
|
|
626
|
-
return Number.isFinite(v) && v > 0 ? v : def;
|
|
627
|
-
};
|
|
628
|
-
var DEFAULT_STORM_CONFIG = {
|
|
629
|
-
deathThreshold: numEnv("SWARM_STORM_DEATHS", 2),
|
|
630
|
-
windowMs: numEnv("SWARM_STORM_WINDOW_MS", 5 * 60 * 1e3),
|
|
631
|
-
spawnFailThreshold: numEnv("SWARM_STORM_SPAWN_FAILS", 2),
|
|
632
|
-
probeIntervalMs: numEnv("SWARM_STORM_PROBE_INTERVAL_MS", 60 * 1e3),
|
|
633
|
-
probeSuccessesToClear: numEnv("SWARM_STORM_PROBE_CLEAR", 2),
|
|
634
|
-
probeTimeoutMs: numEnv("SWARM_STORM_PROBE_TIMEOUT_MS", 20 * 1e3),
|
|
635
|
-
restartCooldownMs: numEnv("SWARM_STORM_RESTART_COOLDOWN_MS", 3 * 60 * 1e3)
|
|
636
|
-
};
|
|
637
757
|
function encodeProbe(rec) {
|
|
638
758
|
return JSON.stringify(rec);
|
|
639
759
|
}
|
|
@@ -657,76 +777,35 @@ function lastProbeReason(recs) {
|
|
|
657
777
|
}
|
|
658
778
|
return null;
|
|
659
779
|
}
|
|
780
|
+
var numEnv, DEFAULT_STORM_CONFIG;
|
|
781
|
+
var init_storm = __esm({
|
|
782
|
+
"src/storm.ts"() {
|
|
783
|
+
"use strict";
|
|
784
|
+
init_src();
|
|
785
|
+
numEnv = (name, def) => {
|
|
786
|
+
const v = Number(process.env[name]);
|
|
787
|
+
return Number.isFinite(v) && v > 0 ? v : def;
|
|
788
|
+
};
|
|
789
|
+
DEFAULT_STORM_CONFIG = {
|
|
790
|
+
deathThreshold: numEnv("SWARM_STORM_DEATHS", 2),
|
|
791
|
+
windowMs: numEnv("SWARM_STORM_WINDOW_MS", 5 * 60 * 1e3),
|
|
792
|
+
spawnFailThreshold: numEnv("SWARM_STORM_SPAWN_FAILS", 2),
|
|
793
|
+
probeIntervalMs: numEnv("SWARM_STORM_PROBE_INTERVAL_MS", 60 * 1e3),
|
|
794
|
+
probeSuccessesToClear: numEnv("SWARM_STORM_PROBE_CLEAR", 2),
|
|
795
|
+
probeTimeoutMs: numEnv("SWARM_STORM_PROBE_TIMEOUT_MS", 20 * 1e3),
|
|
796
|
+
restartCooldownMs: numEnv("SWARM_STORM_RESTART_COOLDOWN_MS", 3 * 60 * 1e3)
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
});
|
|
660
800
|
|
|
661
801
|
// src/db.ts
|
|
662
|
-
var
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
kind TEXT NOT NULL DEFAULT 'drone',
|
|
670
|
-
hero_run_id INTEGER,
|
|
671
|
-
repo TEXT,
|
|
672
|
-
worktree_path TEXT,
|
|
673
|
-
branch TEXT,
|
|
674
|
-
status TEXT NOT NULL DEFAULT 'queued',
|
|
675
|
-
pid INTEGER,
|
|
676
|
-
area TEXT,
|
|
677
|
-
depends_on TEXT,
|
|
678
|
-
model TEXT,
|
|
679
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
680
|
-
pr_url TEXT,
|
|
681
|
-
superseded_by INTEGER,
|
|
682
|
-
decision TEXT,
|
|
683
|
-
stop_reason TEXT,
|
|
684
|
-
respawns INTEGER NOT NULL DEFAULT 0,
|
|
685
|
-
resolves_run_id INTEGER,
|
|
686
|
-
reviews_run_id INTEGER,
|
|
687
|
-
review_verdict TEXT,
|
|
688
|
-
review_cycle INTEGER NOT NULL DEFAULT 0,
|
|
689
|
-
task_title TEXT,
|
|
690
|
-
task_desc TEXT,
|
|
691
|
-
task_url TEXT,
|
|
692
|
-
priority INTEGER NOT NULL DEFAULT 0,
|
|
693
|
-
base_branch TEXT,
|
|
694
|
-
workspace_id TEXT,
|
|
695
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
696
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
697
|
-
);
|
|
698
|
-
CREATE TABLE IF NOT EXISTS questions (
|
|
699
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
700
|
-
run_id INTEGER NOT NULL REFERENCES runs(id),
|
|
701
|
-
text TEXT NOT NULL,
|
|
702
|
-
options TEXT,
|
|
703
|
-
status TEXT NOT NULL DEFAULT 'open',
|
|
704
|
-
answer TEXT,
|
|
705
|
-
answered_by TEXT,
|
|
706
|
-
escalated INTEGER NOT NULL DEFAULT 0,
|
|
707
|
-
asked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
708
|
-
answered_at TEXT
|
|
709
|
-
);
|
|
710
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
711
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
712
|
-
run_id INTEGER REFERENCES runs(id),
|
|
713
|
-
kind TEXT NOT NULL,
|
|
714
|
-
text TEXT,
|
|
715
|
-
actor TEXT,
|
|
716
|
-
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
717
|
-
);
|
|
718
|
-
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
719
|
-
CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
|
|
720
|
-
CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
|
|
721
|
-
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
722
|
-
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
|
|
723
|
-
-- \u043C\u0430\u0440\u043A\u0435\u0440 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u044B\u0445 \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u0411\u0414 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C importDb)
|
|
724
|
-
CREATE TABLE IF NOT EXISTS imports (
|
|
725
|
-
source TEXT PRIMARY KEY,
|
|
726
|
-
run_count INTEGER NOT NULL DEFAULT 0,
|
|
727
|
-
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
728
|
-
);
|
|
729
|
-
`;
|
|
802
|
+
var db_exports = {};
|
|
803
|
+
__export(db_exports, {
|
|
804
|
+
SwarmDb: () => SwarmDb
|
|
805
|
+
});
|
|
806
|
+
import { createRequire as createRequire2 } from "module";
|
|
807
|
+
import { existsSync as existsSync8 } from "fs";
|
|
808
|
+
import { resolve as resolve2 } from "path";
|
|
730
809
|
function rowToQuestion(row) {
|
|
731
810
|
return {
|
|
732
811
|
id: row.id,
|
|
@@ -789,7 +868,7 @@ function isForeignKeyError(err) {
|
|
|
789
868
|
return err instanceof Error && err.code === "SQLITE_CONSTRAINT_FOREIGNKEY";
|
|
790
869
|
}
|
|
791
870
|
function loadBetterSqlite3() {
|
|
792
|
-
const require2 =
|
|
871
|
+
const require2 = createRequire2(import.meta.url);
|
|
793
872
|
try {
|
|
794
873
|
return require2("better-sqlite3");
|
|
795
874
|
} catch (err) {
|
|
@@ -798,668 +877,753 @@ function loadBetterSqlite3() {
|
|
|
798
877
|
);
|
|
799
878
|
}
|
|
800
879
|
}
|
|
801
|
-
var
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
880
|
+
var TERMINAL_SQL, RESOLVER_TERMINAL_SQL, SCHEMA, SwarmDb;
|
|
881
|
+
var init_db = __esm({
|
|
882
|
+
"src/db.ts"() {
|
|
883
|
+
"use strict";
|
|
884
|
+
init_config();
|
|
885
|
+
init_names2();
|
|
886
|
+
init_storm();
|
|
887
|
+
init_src();
|
|
888
|
+
TERMINAL_SQL = TERMINAL_STATUSES.map((s) => `'${s}'`).join(",");
|
|
889
|
+
RESOLVER_TERMINAL_SQL = [...TERMINAL_STATUSES, "stalled"].map((s) => `'${s}'`).join(",");
|
|
890
|
+
SCHEMA = `
|
|
891
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
892
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
893
|
+
task_slug TEXT NOT NULL,
|
|
894
|
+
name TEXT,
|
|
895
|
+
kind TEXT NOT NULL DEFAULT 'drone',
|
|
896
|
+
hero_run_id INTEGER,
|
|
897
|
+
repo TEXT,
|
|
898
|
+
worktree_path TEXT,
|
|
899
|
+
branch TEXT,
|
|
900
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
901
|
+
pid INTEGER,
|
|
902
|
+
area TEXT,
|
|
903
|
+
depends_on TEXT,
|
|
904
|
+
model TEXT,
|
|
905
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
906
|
+
pr_url TEXT,
|
|
907
|
+
superseded_by INTEGER,
|
|
908
|
+
decision TEXT,
|
|
909
|
+
stop_reason TEXT,
|
|
910
|
+
respawns INTEGER NOT NULL DEFAULT 0,
|
|
911
|
+
resolves_run_id INTEGER,
|
|
912
|
+
reviews_run_id INTEGER,
|
|
913
|
+
review_verdict TEXT,
|
|
914
|
+
review_cycle INTEGER NOT NULL DEFAULT 0,
|
|
915
|
+
task_title TEXT,
|
|
916
|
+
task_desc TEXT,
|
|
917
|
+
task_url TEXT,
|
|
918
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
919
|
+
base_branch TEXT,
|
|
920
|
+
workspace_id TEXT,
|
|
921
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
922
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
923
|
+
);
|
|
924
|
+
CREATE TABLE IF NOT EXISTS questions (
|
|
925
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
926
|
+
run_id INTEGER NOT NULL REFERENCES runs(id),
|
|
927
|
+
text TEXT NOT NULL,
|
|
928
|
+
options TEXT,
|
|
929
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
930
|
+
answer TEXT,
|
|
931
|
+
answered_by TEXT,
|
|
932
|
+
escalated INTEGER NOT NULL DEFAULT 0,
|
|
933
|
+
asked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
934
|
+
answered_at TEXT
|
|
935
|
+
);
|
|
936
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
937
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
938
|
+
run_id INTEGER REFERENCES runs(id),
|
|
939
|
+
kind TEXT NOT NULL,
|
|
940
|
+
text TEXT,
|
|
941
|
+
actor TEXT,
|
|
942
|
+
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
943
|
+
);
|
|
944
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
945
|
+
CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
|
|
946
|
+
CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
|
|
947
|
+
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
948
|
+
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
|
|
949
|
+
-- \u043C\u0430\u0440\u043A\u0435\u0440 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u044B\u0445 \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u0411\u0414 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C importDb)
|
|
950
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
951
|
+
source TEXT PRIMARY KEY,
|
|
952
|
+
run_count INTEGER NOT NULL DEFAULT 0,
|
|
953
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
954
|
+
);
|
|
955
|
+
`;
|
|
956
|
+
SwarmDb = class {
|
|
957
|
+
db;
|
|
958
|
+
defaultRepo;
|
|
959
|
+
databaseCtor;
|
|
960
|
+
// для importDb (второй Database, source)
|
|
961
|
+
constructor(path = "swarm.db", defaultRepo = "") {
|
|
962
|
+
this.defaultRepo = normalizeRepo(defaultRepo);
|
|
963
|
+
this.databaseCtor = loadBetterSqlite3();
|
|
964
|
+
this.db = new this.databaseCtor(path);
|
|
965
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
966
|
+
this.db.exec(SCHEMA);
|
|
967
|
+
for (const alter of [
|
|
968
|
+
"ALTER TABLE questions ADD COLUMN options TEXT",
|
|
969
|
+
"ALTER TABLE questions ADD COLUMN escalated INTEGER NOT NULL DEFAULT 0",
|
|
970
|
+
"ALTER TABLE questions ADD COLUMN answered_by TEXT",
|
|
971
|
+
"ALTER TABLE runs ADD COLUMN superseded_by INTEGER",
|
|
972
|
+
"ALTER TABLE runs ADD COLUMN decision TEXT",
|
|
973
|
+
"ALTER TABLE runs ADD COLUMN stop_reason TEXT",
|
|
974
|
+
"ALTER TABLE runs ADD COLUMN repo TEXT",
|
|
975
|
+
"ALTER TABLE runs ADD COLUMN name TEXT",
|
|
976
|
+
"ALTER TABLE runs ADD COLUMN respawns INTEGER NOT NULL DEFAULT 0",
|
|
977
|
+
"ALTER TABLE runs ADD COLUMN resolves_run_id INTEGER",
|
|
978
|
+
"ALTER TABLE runs ADD COLUMN depends_on TEXT",
|
|
979
|
+
"ALTER TABLE runs ADD COLUMN task_title TEXT",
|
|
980
|
+
"ALTER TABLE runs ADD COLUMN task_desc TEXT",
|
|
981
|
+
"ALTER TABLE runs ADD COLUMN task_url TEXT",
|
|
982
|
+
// Иерархия роя (TASWM-78): роль прогона (drone|hero) и ссылка на прогон-героя.
|
|
983
|
+
"ALTER TABLE runs ADD COLUMN kind TEXT NOT NULL DEFAULT 'drone'",
|
|
984
|
+
"ALTER TABLE runs ADD COLUMN hero_run_id INTEGER",
|
|
985
|
+
"ALTER TABLE events ADD COLUMN actor TEXT",
|
|
986
|
+
// Авто-ревью (TASWM-148): прогон-ревьюер (reviews_run_id → review-прогон) и
|
|
987
|
+
// вердикт (approve|changes), который ревьюер пишет на проверяемый прогон.
|
|
988
|
+
"ALTER TABLE runs ADD COLUMN reviews_run_id INTEGER",
|
|
989
|
+
"ALTER TABLE runs ADD COLUMN review_verdict TEXT",
|
|
990
|
+
// Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
|
|
991
|
+
"ALTER TABLE runs ADD COLUMN review_cycle INTEGER NOT NULL DEFAULT 0",
|
|
992
|
+
// Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
|
|
993
|
+
"ALTER TABLE runs ADD COLUMN review_phase TEXT",
|
|
994
|
+
// Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
|
|
995
|
+
"ALTER TABLE runs ADD COLUMN priority INTEGER NOT NULL DEFAULT 0",
|
|
996
|
+
// Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
|
|
997
|
+
"ALTER TABLE runs ADD COLUMN base_branch TEXT",
|
|
998
|
+
// Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
|
|
999
|
+
"ALTER TABLE runs ADD COLUMN workspace_id TEXT"
|
|
1000
|
+
]) {
|
|
1001
|
+
try {
|
|
1002
|
+
this.db.exec(alter);
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_repo ON runs(repo)");
|
|
1007
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_hero ON runs(hero_run_id)");
|
|
1008
|
+
if (this.defaultRepo) {
|
|
1009
|
+
this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
|
|
1010
|
+
}
|
|
849
1011
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_hero ON runs(hero_run_id)");
|
|
853
|
-
if (this.defaultRepo) {
|
|
854
|
-
this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
close() {
|
|
858
|
-
this.db.close();
|
|
859
|
-
}
|
|
860
|
-
// --- runs ---
|
|
861
|
-
createRun(input) {
|
|
862
|
-
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
863
|
-
const res = this.db.prepare(
|
|
864
|
-
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id, reviews_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
865
|
-
).run(
|
|
866
|
-
input.task_slug,
|
|
867
|
-
name,
|
|
868
|
-
input.kind ?? "drone",
|
|
869
|
-
// роль прогона: дефолт — рабочий дрон
|
|
870
|
-
input.hero_run_id ?? null,
|
|
871
|
-
// отряд героя (задел иерархии, TASWM-78)
|
|
872
|
-
normalizeRepo(input.repo || this.defaultRepo),
|
|
873
|
-
// канон при записи → один area-lock на физрепо
|
|
874
|
-
input.area ?? null,
|
|
875
|
-
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
876
|
-
input.model ?? null,
|
|
877
|
-
input.decision ? JSON.stringify(input.decision) : null,
|
|
878
|
-
input.resolves_run_id ?? null,
|
|
879
|
-
input.reviews_run_id ?? null,
|
|
880
|
-
input.review_cycle ?? 0,
|
|
881
|
-
// цикл ревью для ревьюера (авто-ревью v2, TASWM-162); обычный прогон — 0
|
|
882
|
-
input.priority ?? 0,
|
|
883
|
-
// явный приоритет в очереди (TASWM-269); обычный прогон — 0
|
|
884
|
-
input.base_branch ?? null,
|
|
885
|
-
// базовая ветка PR (TASWM-285); null → проектная/дефолтная
|
|
886
|
-
input.workspace_id ?? null
|
|
887
|
-
// воркспейс задачи прогона (TASWM-288); null → глобальный пин
|
|
888
|
-
);
|
|
889
|
-
const id = Number(res.lastInsertRowid);
|
|
890
|
-
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}`;
|
|
891
|
-
this.addEvent(id, "spawned", spawnNote);
|
|
892
|
-
return this.getRun(id);
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* Завести стаб-строку прогона #id, если её ещё нет (TASWM-263, «ревьюеры не могут сдать
|
|
896
|
-
* работу»). Корень: демон в client-режиме (SWARM_BACKEND=client, опубликованный npm-пакет)
|
|
897
|
-
* ведёт runs в in-memory зеркале SwarmClient + проде (swarm_run_upsert), а НЕ в этой sqlite —
|
|
898
|
-
* но дрон-MCP-сервер (droneServer.ts:runDroneServer) ВСЕГДА открывает SwarmDb на локальный
|
|
899
|
-
* DB_PATH, независимо от бэкенда демона. Спавненный из прода прогон (claim/hydrate) в этой
|
|
900
|
-
* БД строки не получает → первый же write-путь (report_step/ask/done) падает на FK
|
|
901
|
-
* (events.run_id/questions.run_id REFERENCES runs(id)), а ревьюер не может ни отчитаться,
|
|
902
|
-
* ни донести вердикт. INSERT OR IGNORE — идемпотентно (конкурентный вызов/уже заведённая
|
|
903
|
-
* строка не трогаются, id explicit-инсертом двигает автоинкремент дальше, коллизий с
|
|
904
|
-
* будущими createRun не будет). status='running' — прогон уже фактически работает (иначе
|
|
905
|
-
* не звал бы write-путь). kind/reviews_run_id/hero_run_id — ЛУЧШЕЕ известное из данных
|
|
906
|
-
* спавна (runDroneServer читает их из SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID,
|
|
907
|
-
* см. launcher.ts): дефолт "drone" без них исказил бы done() ревьюера — тот принял бы свой
|
|
908
|
-
* вердикт за обычную сдачу PR (неверная ветка кода в droneServer.done). name — та же
|
|
909
|
-
* история (TASWM-265): без явного имени колонка оставалась NULL, а rowToRun подставлял
|
|
910
|
-
* `#id` — droneServer.done() подписывал отчёт номером прогона вместо имени дрона (номер
|
|
911
|
-
* локальный и читателю задачи ничего не говорит). Честный fallback — заглушка
|
|
912
|
-
* "(имя неизвестно)", а не номер, выданный за имя.
|
|
913
|
-
*/
|
|
914
|
-
ensureRun(input) {
|
|
915
|
-
this.db.prepare(
|
|
916
|
-
"INSERT OR IGNORE INTO runs (id, task_slug, kind, status, name, reviews_run_id, hero_run_id) VALUES (?, ?, ?, 'running', ?, ?, ?)"
|
|
917
|
-
).run(
|
|
918
|
-
input.id,
|
|
919
|
-
input.task_slug,
|
|
920
|
-
input.kind ?? "drone",
|
|
921
|
-
input.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)",
|
|
922
|
-
input.reviews_run_id ?? null,
|
|
923
|
-
input.hero_run_id ?? null
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
getRun(id) {
|
|
927
|
-
const row = this.db.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
928
|
-
return row ? rowToRun(row) : null;
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Стабильный ключ прогона (TASWM-295). У sqlite он тривиален: id — настоящий PK, живущий
|
|
932
|
-
* в файле, поэтому ключ это и есть номер с префиксом. Префикс не декоративный: он не даёт
|
|
933
|
-
* перепутать пространства ключей, если токен канала, выписанный демоном на sqlite-бэкенде,
|
|
934
|
-
* попадёт в демона на client-бэкенде (там ключи вида `p:<cuid>`) — резолв просто не найдёт
|
|
935
|
-
* прогон, вместо того чтобы наткнуться на чужой с тем же номером.
|
|
936
|
-
*/
|
|
937
|
-
runKeyOf(id) {
|
|
938
|
-
return this.getRun(id) ? `local:${id}` : null;
|
|
939
|
-
}
|
|
940
|
-
runByKey(key) {
|
|
941
|
-
const m = /^local:(\d+)$/.exec(key);
|
|
942
|
-
return m ? this.getRun(Number(m[1])) : null;
|
|
943
|
-
}
|
|
944
|
-
listRuns(status) {
|
|
945
|
-
const rows = status ? this.db.prepare("SELECT * FROM runs WHERE status = ? ORDER BY id DESC").all(status) : this.db.prepare("SELECT * FROM runs ORDER BY id DESC").all();
|
|
946
|
-
return rows.map(rowToRun);
|
|
947
|
-
}
|
|
948
|
-
/** Прогоны отряда Героя-лейтенанта #heroId (его дроны: hero_run_id = heroId), новые сверху (TASWM-81). */
|
|
949
|
-
squadRuns(heroId) {
|
|
950
|
-
const rows = this.db.prepare("SELECT * FROM runs WHERE hero_run_id = ? ORDER BY id DESC").all(heroId);
|
|
951
|
-
return rows.map(rowToRun);
|
|
952
|
-
}
|
|
953
|
-
updateRun(id, patch) {
|
|
954
|
-
const fields = Object.keys(patch);
|
|
955
|
-
if (fields.length === 0) return this.getRun(id);
|
|
956
|
-
const set = fields.map((f) => `${f} = ?`).join(", ");
|
|
957
|
-
const vals = fields.map((f) => {
|
|
958
|
-
const v = patch[f];
|
|
959
|
-
if (f === "depends_on") {
|
|
960
|
-
const arr = v;
|
|
961
|
-
return arr && arr.length ? JSON.stringify(arr) : null;
|
|
1012
|
+
close() {
|
|
1013
|
+
this.db.close();
|
|
962
1014
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
return row.n;
|
|
1028
|
-
}
|
|
1029
|
-
/**
|
|
1030
|
-
* Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
|
|
1031
|
-
* ВСЕГО (любой статус, включая failed/stalled/stopped — специально в отличие от
|
|
1032
|
-
* reviewerCountFor выше) поднималось на review-прогон #reviewRunId В ТЕКУЩЕМ ЦИКЛЕ (скоуп
|
|
1033
|
-
* по циклу — авто-ревью v2, TASWM-162). Зеркалит resolverCountFor — плоский потолок
|
|
1034
|
-
* попыток на цикл. Именно это ловит карусель: reviewerCountFor исключает failed/stalled
|
|
1035
|
-
* (чтобы ОДИН транзиентный сбой не блокировал ревью), но тем самым допускает бесконечный
|
|
1036
|
-
* ретрай, если ревьюер стабильно умирает без done(). Скоуп по циклу — чтобы кап считался
|
|
1037
|
-
* заново на каждой фикс-итерации, а не суммировал попытки прошлых (успешно завершённых)
|
|
1038
|
-
* циклов. superseded — ЕДИНСТВЕННОЕ исключение из капа (TASWM-253): это статус, которым
|
|
1039
|
-
* POST /swarm/review/retry помечает попытки при операторском «попробовать ревью заново» —
|
|
1040
|
-
* без исключения сброс не мог бы ничего сбросить (COUNT(*) слеп к статусу). failed/
|
|
1041
|
-
* stalled/stopped по-прежнему считаются — иначе кап потерял бы смысл на органических
|
|
1042
|
-
* смертях (ровно то, для чего его вводили в TASWM-204).
|
|
1043
|
-
*/
|
|
1044
|
-
reviewerAttemptCountFor(reviewRunId) {
|
|
1045
|
-
const row = this.db.prepare(
|
|
1046
|
-
"SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status != 'superseded' AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0)"
|
|
1047
|
-
).get(reviewRunId, reviewRunId);
|
|
1048
|
-
return row.n;
|
|
1049
|
-
}
|
|
1050
|
-
/** Последняя (любой статус) попытка ревьюера ТЕКУЩЕГО ЦИКЛА на review-прогон #reviewRunId
|
|
1051
|
-
* (TASWM-196; скоуп по циклу — TASWM-162) — см. store.ts:lastReviewerAttemptFor. */
|
|
1052
|
-
lastReviewerAttemptFor(reviewRunId) {
|
|
1053
|
-
const row = this.db.prepare(
|
|
1054
|
-
"SELECT * FROM runs WHERE reviews_run_id = ? AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0) ORDER BY id DESC LIMIT 1"
|
|
1055
|
-
).get(reviewRunId, reviewRunId);
|
|
1056
|
-
return row ? rowToRun(row) : null;
|
|
1057
|
-
}
|
|
1058
|
-
/**
|
|
1059
|
-
* Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
|
|
1060
|
-
* closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
|
|
1061
|
-
* не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
|
|
1062
|
-
*/
|
|
1063
|
-
activeReviewersFor(reviewRunId) {
|
|
1064
|
-
const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
|
|
1065
|
-
return rows.map(rowToRun);
|
|
1066
|
-
}
|
|
1067
|
-
/**
|
|
1068
|
-
* Последний живой (не терминальный) прогон по slug — для swarm cancel. Служебные
|
|
1069
|
-
* сабраны (reviews_run_id/resolves_run_id — ревьюер/резолвер, TASWM-273) исключены:
|
|
1070
|
-
* они заводятся с ТЕМ ЖЕ task_slug, что и родительский прогон, и всегда бо́льшим id
|
|
1071
|
-
* (спавнятся позже) — без исключения `ORDER BY id DESC LIMIT 1` подсовывал бы
|
|
1072
|
-
* cancelRun паузнутого ревьюера (paused НЕ терминален) вместо реального прогона,
|
|
1073
|
-
* которого целился остановить оператор/`swarm cancel <slug>`: безусловный removeWorktree
|
|
1074
|
-
* в cancelRun сносил бы worktree ревьюера, ждущего CI, и переводил его в stopped мимо
|
|
1075
|
-
* обычной цепочки паузы/побудки — найдено при разборе TASWM-273 (PR #564 завис в stalled
|
|
1076
|
-
* без вердикта: worktree ревьюера пропал так, что статус до этого не менялся). Сабран
|
|
1077
|
-
* отменяют через свой id, не через slug — swarm cancel всегда целится в родительский прогон.
|
|
1078
|
-
*/
|
|
1079
|
-
activeRunForSlug(slug) {
|
|
1080
|
-
const row = this.db.prepare(
|
|
1081
|
-
`SELECT * FROM runs WHERE task_slug = ? AND status NOT IN (${TERMINAL_SQL}) AND reviews_run_id IS NULL AND resolves_run_id IS NULL ORDER BY id DESC LIMIT 1`
|
|
1082
|
-
).get(slug);
|
|
1083
|
-
return row ? rowToRun(row) : null;
|
|
1084
|
-
}
|
|
1085
|
-
/**
|
|
1086
|
-
* Зоны, занятые сейчас работающими дронами — для конфликт-осознанной раздачи.
|
|
1087
|
-
* С repo — только в этом репозитории (одна area в разных репо не конфликтует).
|
|
1088
|
-
* Без repo — по всему рою (обратная совместимость).
|
|
1089
|
-
*/
|
|
1090
|
-
runningAreas(repo) {
|
|
1091
|
-
const base3 = "SELECT DISTINCT area FROM runs WHERE status IN ('running','blocked') AND area IS NOT NULL";
|
|
1092
|
-
const rows = repo === void 0 ? this.db.prepare(base3).all() : this.db.prepare(`${base3} AND repo = ?`).all(repo);
|
|
1093
|
-
return rows.map((r) => r.area);
|
|
1094
|
-
}
|
|
1095
|
-
/**
|
|
1096
|
-
* Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
|
|
1097
|
-
* статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
|
|
1098
|
-
* (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
|
|
1099
|
-
* а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
|
|
1100
|
-
* Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
|
|
1101
|
-
* но очередь не виснет (гейт — пропуск в цикле, не блокировка).
|
|
1102
|
-
*/
|
|
1103
|
-
unmetDeps(depends_on) {
|
|
1104
|
-
if (!depends_on || depends_on.length === 0) return [];
|
|
1105
|
-
const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
|
|
1106
|
-
const done = new Set(rows.map((r) => r.task_slug));
|
|
1107
|
-
return depends_on.filter((slug) => !done.has(slug));
|
|
1108
|
-
}
|
|
1109
|
-
/** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
|
|
1110
|
-
distinctRepos() {
|
|
1111
|
-
const rows = this.db.prepare("SELECT DISTINCT repo FROM runs WHERE repo IS NOT NULL AND repo <> ''").all();
|
|
1112
|
-
return rows.map((r) => r.repo);
|
|
1113
|
-
}
|
|
1114
|
-
// --- импорт истории из другой БД (объединение роёв) ---
|
|
1115
|
-
/**
|
|
1116
|
-
* Идемпотентный перенос runs/events/questions из другой swarm-БД в эту.
|
|
1117
|
-
* id прогонов ремапятся (старый→новый); run_id в events/questions и superseded_by
|
|
1118
|
-
* в runs правятся по карте; всем импортируемым прогонам проставляется repo (норм.).
|
|
1119
|
-
* pid НЕ переносим — чужой pid в объединённой БД бессмыслен и мог бы совпасть с
|
|
1120
|
-
* живым процессом (ложный liveness); импортируемые прогоны исторические.
|
|
1121
|
-
* Живые статусы нормализуем (running/blocked→stalled, queued→stopped) и открытые
|
|
1122
|
-
* вопросы не тащим — иначе импорт даёт вечных зомби: без pid death-detect их
|
|
1123
|
-
* пропускает, они навсегда держат (repo,area)-лок и «в работе» в метриках, а их
|
|
1124
|
-
* открытые вопросы «воскрешают» архивный прогон через answer.
|
|
1125
|
-
* Повторный вызов на том же source — no-op (маркер в таблице imports). Source
|
|
1126
|
-
* открываем на ЗАПИСЬ (importDb только ЧИТАЕТ из него): все БД роя — WAL, а bun:sqlite
|
|
1127
|
-
* с readonly НЕ открывает WAL-source без -shm (реальный перенос обычно = копия одного
|
|
1128
|
-
* файла swarm.db без sidecar'ов → SQLITE_CANTOPEN). Несуществующий путь ловим existsSync
|
|
1129
|
-
* ДО открытия — честная ошибка, а не пустой файл (rw создал бы его). Всё в одной
|
|
1130
|
-
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
1131
|
-
*/
|
|
1132
|
-
importDb(sourcePath, repo) {
|
|
1133
|
-
const abs = resolve2(sourcePath);
|
|
1134
|
-
const repoN = normalizeRepo(repo);
|
|
1135
|
-
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
1136
|
-
if (already) return { imported: false, runs: already.run_count };
|
|
1137
|
-
if (!existsSync(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
1138
|
-
const src = new this.databaseCtor(abs);
|
|
1139
|
-
try {
|
|
1140
|
-
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
1141
|
-
const events = src.prepare("SELECT * FROM events ORDER BY id").all();
|
|
1142
|
-
const questions = src.prepare("SELECT * FROM questions ORDER BY id").all();
|
|
1143
|
-
const txn = this.db.transaction(() => {
|
|
1144
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
1145
|
-
const insRun = this.db.prepare(
|
|
1146
|
-
"INSERT INTO runs (task_slug, repo, worktree_path, branch, status, area, model, cost_usd, pr_url, decision, stop_reason, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
1015
|
+
// --- runs ---
|
|
1016
|
+
createRun(input) {
|
|
1017
|
+
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
1018
|
+
const res = this.db.prepare(
|
|
1019
|
+
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id, reviews_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
1020
|
+
).run(
|
|
1021
|
+
input.task_slug,
|
|
1022
|
+
name,
|
|
1023
|
+
input.kind ?? "drone",
|
|
1024
|
+
// роль прогона: дефолт — рабочий дрон
|
|
1025
|
+
input.hero_run_id ?? null,
|
|
1026
|
+
// отряд героя (задел иерархии, TASWM-78)
|
|
1027
|
+
normalizeRepo(input.repo || this.defaultRepo),
|
|
1028
|
+
// канон при записи → один area-lock на физрепо
|
|
1029
|
+
input.area ?? null,
|
|
1030
|
+
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
1031
|
+
input.model ?? null,
|
|
1032
|
+
input.decision ? JSON.stringify(input.decision) : null,
|
|
1033
|
+
input.resolves_run_id ?? null,
|
|
1034
|
+
input.reviews_run_id ?? null,
|
|
1035
|
+
input.review_cycle ?? 0,
|
|
1036
|
+
// цикл ревью для ревьюера (авто-ревью v2, TASWM-162); обычный прогон — 0
|
|
1037
|
+
input.priority ?? 0,
|
|
1038
|
+
// явный приоритет в очереди (TASWM-269); обычный прогон — 0
|
|
1039
|
+
input.base_branch ?? null,
|
|
1040
|
+
// базовая ветка PR (TASWM-285); null → проектная/дефолтная
|
|
1041
|
+
input.workspace_id ?? null
|
|
1042
|
+
// воркспейс задачи прогона (TASWM-288); null → глобальный пин
|
|
1043
|
+
);
|
|
1044
|
+
const id = Number(res.lastInsertRowid);
|
|
1045
|
+
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}`;
|
|
1046
|
+
this.addEvent(id, "spawned", spawnNote);
|
|
1047
|
+
return this.getRun(id);
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Завести стаб-строку прогона #id, если её ещё нет (TASWM-263, «ревьюеры не могут сдать
|
|
1051
|
+
* работу»). Корень: демон в client-режиме (SWARM_BACKEND=client, опубликованный npm-пакет)
|
|
1052
|
+
* ведёт runs в in-memory зеркале SwarmClient + проде (swarm_run_upsert), а НЕ в этой sqlite —
|
|
1053
|
+
* но дрон-MCP-сервер (droneServer.ts:runDroneServer) ВСЕГДА открывает SwarmDb на локальный
|
|
1054
|
+
* DB_PATH, независимо от бэкенда демона. Спавненный из прода прогон (claim/hydrate) в этой
|
|
1055
|
+
* БД строки не получает → первый же write-путь (report_step/ask/done) падает на FK
|
|
1056
|
+
* (events.run_id/questions.run_id REFERENCES runs(id)), а ревьюер не может ни отчитаться,
|
|
1057
|
+
* ни донести вердикт. INSERT OR IGNORE — идемпотентно (конкурентный вызов/уже заведённая
|
|
1058
|
+
* строка не трогаются, id explicit-инсертом двигает автоинкремент дальше, коллизий с
|
|
1059
|
+
* будущими createRun не будет). status='running' — прогон уже фактически работает (иначе
|
|
1060
|
+
* не звал бы write-путь). kind/reviews_run_id/hero_run_id — ЛУЧШЕЕ известное из данных
|
|
1061
|
+
* спавна (runDroneServer читает их из SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID,
|
|
1062
|
+
* см. launcher.ts): дефолт "drone" без них исказил бы done() ревьюера — тот принял бы свой
|
|
1063
|
+
* вердикт за обычную сдачу PR (неверная ветка кода в droneServer.done). name — та же
|
|
1064
|
+
* история (TASWM-265): без явного имени колонка оставалась NULL, а rowToRun подставлял
|
|
1065
|
+
* `#id` — droneServer.done() подписывал отчёт номером прогона вместо имени дрона (номер
|
|
1066
|
+
* локальный и читателю задачи ничего не говорит). Честный fallback — заглушка
|
|
1067
|
+
* "(имя неизвестно)", а не номер, выданный за имя.
|
|
1068
|
+
*/
|
|
1069
|
+
ensureRun(input) {
|
|
1070
|
+
this.db.prepare(
|
|
1071
|
+
"INSERT OR IGNORE INTO runs (id, task_slug, kind, status, name, reviews_run_id, hero_run_id) VALUES (?, ?, ?, 'running', ?, ?, ?)"
|
|
1072
|
+
).run(
|
|
1073
|
+
input.id,
|
|
1074
|
+
input.task_slug,
|
|
1075
|
+
input.kind ?? "drone",
|
|
1076
|
+
input.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)",
|
|
1077
|
+
input.reviews_run_id ?? null,
|
|
1078
|
+
input.hero_run_id ?? null
|
|
1147
1079
|
);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1080
|
+
}
|
|
1081
|
+
getRun(id) {
|
|
1082
|
+
const row = this.db.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
1083
|
+
return row ? rowToRun(row) : null;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Стабильный ключ прогона (TASWM-295). У sqlite он тривиален: id — настоящий PK, живущий
|
|
1087
|
+
* в файле, поэтому ключ это и есть номер с префиксом. Префикс не декоративный: он не даёт
|
|
1088
|
+
* перепутать пространства ключей, если токен канала, выписанный демоном на sqlite-бэкенде,
|
|
1089
|
+
* попадёт в демона на client-бэкенде (там ключи вида `p:<cuid>`) — резолв просто не найдёт
|
|
1090
|
+
* прогон, вместо того чтобы наткнуться на чужой с тем же номером.
|
|
1091
|
+
*/
|
|
1092
|
+
runKeyOf(id) {
|
|
1093
|
+
return this.getRun(id) ? `local:${id}` : null;
|
|
1094
|
+
}
|
|
1095
|
+
runByKey(key) {
|
|
1096
|
+
const m = /^local:(\d+)$/.exec(key);
|
|
1097
|
+
return m ? this.getRun(Number(m[1])) : null;
|
|
1098
|
+
}
|
|
1099
|
+
listRuns(status) {
|
|
1100
|
+
const rows = status ? this.db.prepare("SELECT * FROM runs WHERE status = ? ORDER BY id DESC").all(status) : this.db.prepare("SELECT * FROM runs ORDER BY id DESC").all();
|
|
1101
|
+
return rows.map(rowToRun);
|
|
1102
|
+
}
|
|
1103
|
+
/** Прогоны отряда Героя-лейтенанта #heroId (его дроны: hero_run_id = heroId), новые сверху (TASWM-81). */
|
|
1104
|
+
squadRuns(heroId) {
|
|
1105
|
+
const rows = this.db.prepare("SELECT * FROM runs WHERE hero_run_id = ? ORDER BY id DESC").all(heroId);
|
|
1106
|
+
return rows.map(rowToRun);
|
|
1107
|
+
}
|
|
1108
|
+
updateRun(id, patch) {
|
|
1109
|
+
const fields = Object.keys(patch);
|
|
1110
|
+
if (fields.length === 0) return this.getRun(id);
|
|
1111
|
+
const set = fields.map((f) => `${f} = ?`).join(", ");
|
|
1112
|
+
const vals = fields.map((f) => {
|
|
1113
|
+
const v = patch[f];
|
|
1114
|
+
if (f === "depends_on") {
|
|
1115
|
+
const arr = v;
|
|
1116
|
+
return arr && arr.length ? JSON.stringify(arr) : null;
|
|
1160
1117
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1118
|
+
return v;
|
|
1119
|
+
});
|
|
1120
|
+
this.db.prepare(`UPDATE runs SET ${set}, updated_at = datetime('now') WHERE id = ?`).run(...vals, id);
|
|
1121
|
+
return this.getRun(id);
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Атомарно увеличить счётчик авто-оживлений прогона (после смерти дрона рой
|
|
1125
|
+
* сам его respawn'ит/--continue'ит). Возвращает новое значение — по нему
|
|
1126
|
+
* death-detect решает, не исчерпан ли лимит (2 оживления на прогон).
|
|
1127
|
+
*/
|
|
1128
|
+
incRespawn(id) {
|
|
1129
|
+
this.db.prepare("UPDATE runs SET respawns = respawns + 1, updated_at = datetime('now') WHERE id = ?").run(id);
|
|
1130
|
+
const row = this.db.prepare("SELECT respawns FROM runs WHERE id = ?").get(id);
|
|
1131
|
+
return row?.respawns ?? 0;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Остановить прогон оператором: статус "stopped" + причина + событие.
|
|
1135
|
+
* Прогон НЕ удаляется — остаётся в БД и в истории навсегда (в отличие от failed
|
|
1136
|
+
* с выброшенным worktree мы храним причину и запись). Kill/cleanup — снаружи (cancelRun).
|
|
1137
|
+
*/
|
|
1138
|
+
cancel(id, reason) {
|
|
1139
|
+
const run = this.updateRun(id, { status: "stopped", stop_reason: reason });
|
|
1140
|
+
if (run) this.addEvent(id, "stopped", reason);
|
|
1141
|
+
return run;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Живой прогон-резолвер конфликта для review-прогона #id (если есть). «Живой» =
|
|
1145
|
+
* не в терминальном статусе: пока такой в работе/на ревью — второго резолвера на тот
|
|
1146
|
+
* же PR не поднимаем (иначе на каждый тик плодили бы дронов). stalled/failed/done —
|
|
1147
|
+
* не живые: резолвер сорвался/добил → можно, если не исчёрпан лимит попыток.
|
|
1148
|
+
*/
|
|
1149
|
+
activeResolverFor(reviewRunId) {
|
|
1150
|
+
const row = this.db.prepare(
|
|
1151
|
+
`SELECT * FROM runs WHERE resolves_run_id = ? AND status NOT IN (${RESOLVER_TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
|
|
1152
|
+
).get(reviewRunId);
|
|
1153
|
+
return row ? rowToRun(row) : null;
|
|
1154
|
+
}
|
|
1155
|
+
/** Сколько всего резолверов уже поднимали на этот review-PR — потолок авто-попыток. */
|
|
1156
|
+
resolverCountFor(reviewRunId) {
|
|
1157
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
|
|
1158
|
+
return row.n;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Сколько ЖИВЫХ/УСПЕШНЫХ ревьюеров уже поднимали на этот review-PR В ТЕКУЩЕМ ЦИКЛЕ
|
|
1162
|
+
* (авто-ревью, TASWM-148; скоуп по циклу — авто-ревью v2, TASWM-162) — one-shot
|
|
1163
|
+
* идемпотентность БЕЗ лимита попыток (в отличие от резолвера): >0 → в этом цикле ревьюер
|
|
1164
|
+
* уже есть, повторно не спавним. failed/stalled ревьюеры (worktree/launch сорвались ДО
|
|
1165
|
+
* вердикта) в счёт НЕ идут — иначе один транзиентный сбой спавна (диск/git/gh) навсегда
|
|
1166
|
+
* блокировал бы ревью цикла без шанса на повтор. stopped/superseded — тоже НЕ считаем
|
|
1167
|
+
* (TASWM-253): stopped — оператор закрыл зомби-ревьюера руками (POST /swarm/runs/cleanup-
|
|
1168
|
+
* dead), superseded — оператор явно сбросил попытки ревью (POST /swarm/review/retry,
|
|
1169
|
+
* см. packages/api/src/swarm/routes.ts); оба варианта означают «эта попытка не в счёт»,
|
|
1170
|
+
* а не «ревьюер ещё жив». РАНЬШЕ stopped
|
|
1171
|
+
* тут не исключался — операторский kill зомби-ревьюера навсегда держал гейт закрытым
|
|
1172
|
+
* (reviewerCountFor>0 бесконечно), хотя счётчик попыток (ниже) уже не рос. Скоуп «текущий
|
|
1173
|
+
* цикл» = review_cycle ревьюера равен review_cycle проверяемого прогона: после фикс-
|
|
1174
|
+
* итерации исполнитель бампит цикл (droneServer.done), ревьюеры прошлого цикла отваливаются
|
|
1175
|
+
* из счёта → гейт снова открыт для ре-ревьюера (без него бейдж «changes» держал бы слот
|
|
1176
|
+
* навсегда).
|
|
1177
|
+
*/
|
|
1178
|
+
reviewerCountFor(reviewRunId) {
|
|
1179
|
+
const row = this.db.prepare(
|
|
1180
|
+
"SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status NOT IN ('failed','stalled','stopped','superseded') AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0)"
|
|
1181
|
+
).get(reviewRunId, reviewRunId);
|
|
1182
|
+
return row.n;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
|
|
1186
|
+
* ВСЕГО (любой статус, включая failed/stalled/stopped — специально в отличие от
|
|
1187
|
+
* reviewerCountFor выше) поднималось на review-прогон #reviewRunId В ТЕКУЩЕМ ЦИКЛЕ (скоуп
|
|
1188
|
+
* по циклу — авто-ревью v2, TASWM-162). Зеркалит resolverCountFor — плоский потолок
|
|
1189
|
+
* попыток на цикл. Именно это ловит карусель: reviewerCountFor исключает failed/stalled
|
|
1190
|
+
* (чтобы ОДИН транзиентный сбой не блокировал ревью), но тем самым допускает бесконечный
|
|
1191
|
+
* ретрай, если ревьюер стабильно умирает без done(). Скоуп по циклу — чтобы кап считался
|
|
1192
|
+
* заново на каждой фикс-итерации, а не суммировал попытки прошлых (успешно завершённых)
|
|
1193
|
+
* циклов. superseded — ЕДИНСТВЕННОЕ исключение из капа (TASWM-253): это статус, которым
|
|
1194
|
+
* POST /swarm/review/retry помечает попытки при операторском «попробовать ревью заново» —
|
|
1195
|
+
* без исключения сброс не мог бы ничего сбросить (COUNT(*) слеп к статусу). failed/
|
|
1196
|
+
* stalled/stopped по-прежнему считаются — иначе кап потерял бы смысл на органических
|
|
1197
|
+
* смертях (ровно то, для чего его вводили в TASWM-204).
|
|
1198
|
+
*/
|
|
1199
|
+
reviewerAttemptCountFor(reviewRunId) {
|
|
1200
|
+
const row = this.db.prepare(
|
|
1201
|
+
"SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status != 'superseded' AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0)"
|
|
1202
|
+
).get(reviewRunId, reviewRunId);
|
|
1203
|
+
return row.n;
|
|
1204
|
+
}
|
|
1205
|
+
/** Последняя (любой статус) попытка ревьюера ТЕКУЩЕГО ЦИКЛА на review-прогон #reviewRunId
|
|
1206
|
+
* (TASWM-196; скоуп по циклу — TASWM-162) — см. store.ts:lastReviewerAttemptFor. */
|
|
1207
|
+
lastReviewerAttemptFor(reviewRunId) {
|
|
1208
|
+
const row = this.db.prepare(
|
|
1209
|
+
"SELECT * FROM runs WHERE reviews_run_id = ? AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0) ORDER BY id DESC LIMIT 1"
|
|
1210
|
+
).get(reviewRunId, reviewRunId);
|
|
1211
|
+
return row ? rowToRun(row) : null;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
|
|
1215
|
+
* closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
|
|
1216
|
+
* не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
|
|
1217
|
+
*/
|
|
1218
|
+
activeReviewersFor(reviewRunId) {
|
|
1219
|
+
const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
|
|
1220
|
+
return rows.map(rowToRun);
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Последний живой (не терминальный) прогон по slug — для swarm cancel. Служебные
|
|
1224
|
+
* сабраны (reviews_run_id/resolves_run_id — ревьюер/резолвер, TASWM-273) исключены:
|
|
1225
|
+
* они заводятся с ТЕМ ЖЕ task_slug, что и родительский прогон, и всегда бо́льшим id
|
|
1226
|
+
* (спавнятся позже) — без исключения `ORDER BY id DESC LIMIT 1` подсовывал бы
|
|
1227
|
+
* cancelRun паузнутого ревьюера (paused НЕ терминален) вместо реального прогона,
|
|
1228
|
+
* которого целился остановить оператор/`swarm cancel <slug>`: безусловный removeWorktree
|
|
1229
|
+
* в cancelRun сносил бы worktree ревьюера, ждущего CI, и переводил его в stopped мимо
|
|
1230
|
+
* обычной цепочки паузы/побудки — найдено при разборе TASWM-273 (PR #564 завис в stalled
|
|
1231
|
+
* без вердикта: worktree ревьюера пропал так, что статус до этого не менялся). Сабран
|
|
1232
|
+
* отменяют через свой id, не через slug — swarm cancel всегда целится в родительский прогон.
|
|
1233
|
+
*/
|
|
1234
|
+
activeRunForSlug(slug) {
|
|
1235
|
+
const row = this.db.prepare(
|
|
1236
|
+
`SELECT * FROM runs WHERE task_slug = ? AND status NOT IN (${TERMINAL_SQL}) AND reviews_run_id IS NULL AND resolves_run_id IS NULL ORDER BY id DESC LIMIT 1`
|
|
1237
|
+
).get(slug);
|
|
1238
|
+
return row ? rowToRun(row) : null;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Зоны, занятые сейчас работающими дронами — для конфликт-осознанной раздачи.
|
|
1242
|
+
* С repo — только в этом репозитории (одна area в разных репо не конфликтует).
|
|
1243
|
+
* Без repo — по всему рою (обратная совместимость).
|
|
1244
|
+
*/
|
|
1245
|
+
runningAreas(repo) {
|
|
1246
|
+
const base3 = "SELECT DISTINCT area FROM runs WHERE status IN ('running','blocked') AND area IS NOT NULL";
|
|
1247
|
+
const rows = repo === void 0 ? this.db.prepare(base3).all() : this.db.prepare(`${base3} AND repo = ?`).all(repo);
|
|
1248
|
+
return rows.map((r) => r.area);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
|
|
1252
|
+
* статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
|
|
1253
|
+
* (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
|
|
1254
|
+
* а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
|
|
1255
|
+
* Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
|
|
1256
|
+
* но очередь не виснет (гейт — пропуск в цикле, не блокировка).
|
|
1257
|
+
*/
|
|
1258
|
+
unmetDeps(depends_on) {
|
|
1259
|
+
if (!depends_on || depends_on.length === 0) return [];
|
|
1260
|
+
const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
|
|
1261
|
+
const done = new Set(rows.map((r) => r.task_slug));
|
|
1262
|
+
return depends_on.filter((slug) => !done.has(slug));
|
|
1263
|
+
}
|
|
1264
|
+
/** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
|
|
1265
|
+
distinctRepos() {
|
|
1266
|
+
const rows = this.db.prepare("SELECT DISTINCT repo FROM runs WHERE repo IS NOT NULL AND repo <> ''").all();
|
|
1267
|
+
return rows.map((r) => r.repo);
|
|
1268
|
+
}
|
|
1269
|
+
// --- импорт истории из другой БД (объединение роёв) ---
|
|
1270
|
+
/**
|
|
1271
|
+
* Идемпотентный перенос runs/events/questions из другой swarm-БД в эту.
|
|
1272
|
+
* id прогонов ремапятся (старый→новый); run_id в events/questions и superseded_by
|
|
1273
|
+
* в runs правятся по карте; всем импортируемым прогонам проставляется repo (норм.).
|
|
1274
|
+
* pid НЕ переносим — чужой pid в объединённой БД бессмыслен и мог бы совпасть с
|
|
1275
|
+
* живым процессом (ложный liveness); импортируемые прогоны исторические.
|
|
1276
|
+
* Живые статусы нормализуем (running/blocked→stalled, queued→stopped) и открытые
|
|
1277
|
+
* вопросы не тащим — иначе импорт даёт вечных зомби: без pid death-detect их
|
|
1278
|
+
* пропускает, они навсегда держат (repo,area)-лок и «в работе» в метриках, а их
|
|
1279
|
+
* открытые вопросы «воскрешают» архивный прогон через answer.
|
|
1280
|
+
* Повторный вызов на том же source — no-op (маркер в таблице imports). Source
|
|
1281
|
+
* открываем на ЗАПИСЬ (importDb только ЧИТАЕТ из него): все БД роя — WAL, а bun:sqlite
|
|
1282
|
+
* с readonly НЕ открывает WAL-source без -shm (реальный перенос обычно = копия одного
|
|
1283
|
+
* файла swarm.db без sidecar'ов → SQLITE_CANTOPEN). Несуществующий путь ловим existsSync
|
|
1284
|
+
* ДО открытия — честная ошибка, а не пустой файл (rw создал бы его). Всё в одной
|
|
1285
|
+
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
1286
|
+
*/
|
|
1287
|
+
importDb(sourcePath, repo) {
|
|
1288
|
+
const abs = resolve2(sourcePath);
|
|
1289
|
+
const repoN = normalizeRepo(repo);
|
|
1290
|
+
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
1291
|
+
if (already) return { imported: false, runs: already.run_count };
|
|
1292
|
+
if (!existsSync8(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
1293
|
+
const src = new this.databaseCtor(abs);
|
|
1294
|
+
try {
|
|
1295
|
+
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
1296
|
+
const events = src.prepare("SELECT * FROM events ORDER BY id").all();
|
|
1297
|
+
const questions = src.prepare("SELECT * FROM questions ORDER BY id").all();
|
|
1298
|
+
const txn = this.db.transaction(() => {
|
|
1299
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1300
|
+
const insRun = this.db.prepare(
|
|
1301
|
+
"INSERT INTO runs (task_slug, repo, worktree_path, branch, status, area, model, cost_usd, pr_url, decision, stop_reason, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
1302
|
+
);
|
|
1303
|
+
for (const r of runs) {
|
|
1304
|
+
const raw = r.status ?? "done";
|
|
1305
|
+
let status = raw;
|
|
1306
|
+
let stopReason = r.stop_reason ?? null;
|
|
1307
|
+
let normEvent = null;
|
|
1308
|
+
if (raw === "running" || raw === "blocked") {
|
|
1309
|
+
status = "stalled";
|
|
1310
|
+
normEvent = { kind: "stalled", text: "\u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D \u0438\u0437 \u0430\u0440\u0445\u0438\u0432\u0430: \u0441\u0442\u0430\u0442\u0443\u0441 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u043E\u0432\u0430\u043D" };
|
|
1311
|
+
} else if (raw === "queued") {
|
|
1312
|
+
status = "stopped";
|
|
1313
|
+
stopReason = "\u0430\u0440\u0445\u0438\u0432\u043D\u044B\u0439 \u0438\u043C\u043F\u043E\u0440\u0442";
|
|
1314
|
+
normEvent = { kind: "stopped", text: "\u0430\u0440\u0445\u0438\u0432\u043D\u044B\u0439 \u0438\u043C\u043F\u043E\u0440\u0442" };
|
|
1315
|
+
}
|
|
1316
|
+
const res = insRun.run(
|
|
1317
|
+
r.task_slug,
|
|
1318
|
+
repoN,
|
|
1319
|
+
r.worktree_path ?? null,
|
|
1320
|
+
r.branch ?? null,
|
|
1321
|
+
status,
|
|
1322
|
+
r.area ?? null,
|
|
1323
|
+
r.model ?? null,
|
|
1324
|
+
r.cost_usd ?? 0,
|
|
1325
|
+
r.pr_url ?? null,
|
|
1326
|
+
r.decision ?? null,
|
|
1327
|
+
stopReason,
|
|
1328
|
+
r.created_at ?? null,
|
|
1329
|
+
r.updated_at ?? null
|
|
1330
|
+
);
|
|
1331
|
+
const newId = Number(res.lastInsertRowid);
|
|
1332
|
+
idMap.set(r.id, newId);
|
|
1333
|
+
if (normEvent) this.addEvent(newId, normEvent.kind, normEvent.text);
|
|
1334
|
+
}
|
|
1335
|
+
const setSup = this.db.prepare("UPDATE runs SET superseded_by = ? WHERE id = ?");
|
|
1336
|
+
for (const r of runs) {
|
|
1337
|
+
const sup = r.superseded_by;
|
|
1338
|
+
if (sup != null && idMap.has(sup)) setSup.run(idMap.get(sup), idMap.get(r.id));
|
|
1339
|
+
}
|
|
1340
|
+
const insEv = this.db.prepare("INSERT INTO events (run_id, kind, text, actor, at) VALUES (?,?,?,?,?)");
|
|
1341
|
+
for (const e of events) {
|
|
1342
|
+
const oldRun = e.run_id;
|
|
1343
|
+
const newRun = oldRun != null ? idMap.get(oldRun) ?? null : null;
|
|
1344
|
+
insEv.run(newRun, e.kind, e.text ?? null, e.actor ?? null, e.at ?? null);
|
|
1345
|
+
}
|
|
1346
|
+
const insQ = this.db.prepare(
|
|
1347
|
+
"INSERT INTO questions (run_id, text, options, status, answer, answered_by, escalated, asked_at, answered_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
|
1348
|
+
);
|
|
1349
|
+
for (const q of questions) {
|
|
1350
|
+
if (q.status === "open") continue;
|
|
1351
|
+
const newRun = idMap.get(q.run_id);
|
|
1352
|
+
if (newRun == null) continue;
|
|
1353
|
+
insQ.run(
|
|
1354
|
+
newRun,
|
|
1355
|
+
q.text,
|
|
1356
|
+
q.options ?? null,
|
|
1357
|
+
q.status ?? "answered",
|
|
1358
|
+
q.answer ?? null,
|
|
1359
|
+
q.answered_by ?? null,
|
|
1360
|
+
q.escalated ?? 0,
|
|
1361
|
+
q.asked_at ?? null,
|
|
1362
|
+
q.answered_at ?? null
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
this.db.prepare("INSERT INTO imports (source, run_count) VALUES (?, ?)").run(abs, runs.length);
|
|
1366
|
+
});
|
|
1367
|
+
txn();
|
|
1368
|
+
return { imported: true, runs: runs.length };
|
|
1369
|
+
} finally {
|
|
1370
|
+
src.close();
|
|
1184
1371
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1372
|
+
}
|
|
1373
|
+
// --- questions (петля ask/answer) ---
|
|
1374
|
+
ask(run_id, text, options) {
|
|
1375
|
+
const insert = () => this.db.prepare("INSERT INTO questions (run_id, text, options) VALUES (?, ?, ?)").run(run_id, text, options && options.length ? JSON.stringify(options) : null);
|
|
1376
|
+
let res;
|
|
1377
|
+
try {
|
|
1378
|
+
res = insert();
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
if (isForeignKeyError(err)) {
|
|
1381
|
+
console.error(
|
|
1382
|
+
`[swarm-db] FOREIGN KEY constraint failed: \u0432\u043E\u043F\u0440\u043E\u0441 \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run_id} \u2014 \u0441\u0442\u0440\u043E\u043A\u0438 \u043D\u0435\u0442 \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u043C \u0441\u0442\u043E\u0440\u0435 (\u0441\u043C. TASWM-263: \u0434\u0435\u043C\u043E\u043D \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u043D\u0435 \u0437\u0430\u0432\u043E\u0434\u0438\u0442 \u0435\u0451 \u0437\u0434\u0435\u0441\u044C). \u0417\u0430\u0432\u043E\u0436\u0443 \u0441\u0442\u0430\u0431 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E.`
|
|
1383
|
+
);
|
|
1384
|
+
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1385
|
+
res = insert();
|
|
1386
|
+
} else {
|
|
1387
|
+
throw err;
|
|
1388
|
+
}
|
|
1190
1389
|
}
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
);
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1390
|
+
const id = Number(res.lastInsertRowid);
|
|
1391
|
+
this.updateRun(run_id, { status: "blocked" });
|
|
1392
|
+
this.addEvent(run_id, "blocked", text);
|
|
1393
|
+
return this.getQuestion(id);
|
|
1394
|
+
}
|
|
1395
|
+
/** Оркестратор поднял вопрос человеку (техническое решил бы сам). */
|
|
1396
|
+
escalateQuestion(id) {
|
|
1397
|
+
this.db.prepare("UPDATE questions SET escalated=1 WHERE id=?").run(id);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Снять открытый вопрос без реального ответа (дрон замер: overmind недоступен,
|
|
1401
|
+
* ответа не будет — TASWM-41). Отдельный статус 'withdrawn' (не 'answered'): вопрос
|
|
1402
|
+
* уходит из openQuestions() (WHERE status='open'), защищён от answer() (тот правит
|
|
1403
|
+
* только open И воскрешает прогон лишь на статусе 'answered') — так снятый вопрос
|
|
1404
|
+
* не оживит замерший прогон. Остаётся в истории как снятый с пояснением.
|
|
1405
|
+
*/
|
|
1406
|
+
withdrawQuestion(id, reason) {
|
|
1407
|
+
this.db.prepare("UPDATE questions SET status='withdrawn', answer=?, answered_at=datetime('now') WHERE id=? AND status='open'").run(reason, id);
|
|
1408
|
+
}
|
|
1409
|
+
answer(question_id, answer, by = "human") {
|
|
1410
|
+
this.db.prepare(
|
|
1411
|
+
"UPDATE questions SET status='answered', answer=?, answered_by=?, answered_at=datetime('now') WHERE id=? AND status='open'"
|
|
1412
|
+
).run(answer, by, question_id);
|
|
1413
|
+
const q = this.getQuestion(question_id);
|
|
1414
|
+
if (q && q.status === "answered") {
|
|
1415
|
+
this.updateRun(q.run_id, { status: "running" });
|
|
1416
|
+
this.addEvent(q.run_id, "answered", answer, by);
|
|
1209
1417
|
}
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
} catch (err) {
|
|
1225
|
-
if (isForeignKeyError(err)) {
|
|
1226
|
-
console.error(
|
|
1227
|
-
`[swarm-db] FOREIGN KEY constraint failed: \u0432\u043E\u043F\u0440\u043E\u0441 \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run_id} \u2014 \u0441\u0442\u0440\u043E\u043A\u0438 \u043D\u0435\u0442 \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u043C \u0441\u0442\u043E\u0440\u0435 (\u0441\u043C. TASWM-263: \u0434\u0435\u043C\u043E\u043D \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u043D\u0435 \u0437\u0430\u0432\u043E\u0434\u0438\u0442 \u0435\u0451 \u0437\u0434\u0435\u0441\u044C). \u0417\u0430\u0432\u043E\u0436\u0443 \u0441\u0442\u0430\u0431 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E.`
|
|
1418
|
+
return q;
|
|
1419
|
+
}
|
|
1420
|
+
/** Пометить старый прогон переигранным на новый (откат на другой вариант). */
|
|
1421
|
+
supersede(oldId, newId, by = "human") {
|
|
1422
|
+
this.updateRun(oldId, { superseded_by: newId });
|
|
1423
|
+
this.addEvent(oldId, "superseded", `\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u2192 #${newId}`, by);
|
|
1424
|
+
}
|
|
1425
|
+
getQuestion(id) {
|
|
1426
|
+
const row = this.db.prepare("SELECT * FROM questions WHERE id = ?").get(id);
|
|
1427
|
+
return row ? rowToQuestion(row) : null;
|
|
1428
|
+
}
|
|
1429
|
+
openQuestions() {
|
|
1430
|
+
return this.db.prepare("SELECT * FROM questions WHERE status='open' ORDER BY id").all().map(
|
|
1431
|
+
rowToQuestion
|
|
1228
1432
|
);
|
|
1229
|
-
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1230
|
-
res = insert();
|
|
1231
|
-
} else {
|
|
1232
|
-
throw err;
|
|
1233
1433
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
return this.getQuestion(id);
|
|
1239
|
-
}
|
|
1240
|
-
/** Оркестратор поднял вопрос человеку (техническое решил бы сам). */
|
|
1241
|
-
escalateQuestion(id) {
|
|
1242
|
-
this.db.prepare("UPDATE questions SET escalated=1 WHERE id=?").run(id);
|
|
1243
|
-
}
|
|
1244
|
-
/**
|
|
1245
|
-
* Снять открытый вопрос без реального ответа (дрон замер: overmind недоступен,
|
|
1246
|
-
* ответа не будет — TASWM-41). Отдельный статус 'withdrawn' (не 'answered'): вопрос
|
|
1247
|
-
* уходит из openQuestions() (WHERE status='open'), защищён от answer() (тот правит
|
|
1248
|
-
* только open И воскрешает прогон лишь на статусе 'answered') — так снятый вопрос
|
|
1249
|
-
* не оживит замерший прогон. Остаётся в истории как снятый с пояснением.
|
|
1250
|
-
*/
|
|
1251
|
-
withdrawQuestion(id, reason) {
|
|
1252
|
-
this.db.prepare("UPDATE questions SET status='withdrawn', answer=?, answered_at=datetime('now') WHERE id=? AND status='open'").run(reason, id);
|
|
1253
|
-
}
|
|
1254
|
-
answer(question_id, answer, by = "human") {
|
|
1255
|
-
this.db.prepare(
|
|
1256
|
-
"UPDATE questions SET status='answered', answer=?, answered_by=?, answered_at=datetime('now') WHERE id=? AND status='open'"
|
|
1257
|
-
).run(answer, by, question_id);
|
|
1258
|
-
const q = this.getQuestion(question_id);
|
|
1259
|
-
if (q && q.status === "answered") {
|
|
1260
|
-
this.updateRun(q.run_id, { status: "running" });
|
|
1261
|
-
this.addEvent(q.run_id, "answered", answer, by);
|
|
1262
|
-
}
|
|
1263
|
-
return q;
|
|
1264
|
-
}
|
|
1265
|
-
/** Пометить старый прогон переигранным на новый (откат на другой вариант). */
|
|
1266
|
-
supersede(oldId, newId, by = "human") {
|
|
1267
|
-
this.updateRun(oldId, { superseded_by: newId });
|
|
1268
|
-
this.addEvent(oldId, "superseded", `\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u2192 #${newId}`, by);
|
|
1269
|
-
}
|
|
1270
|
-
getQuestion(id) {
|
|
1271
|
-
const row = this.db.prepare("SELECT * FROM questions WHERE id = ?").get(id);
|
|
1272
|
-
return row ? rowToQuestion(row) : null;
|
|
1273
|
-
}
|
|
1274
|
-
openQuestions() {
|
|
1275
|
-
return this.db.prepare("SELECT * FROM questions WHERE status='open' ORDER BY id").all().map(
|
|
1276
|
-
rowToQuestion
|
|
1277
|
-
);
|
|
1278
|
-
}
|
|
1279
|
-
/** ВСЕ вопросы прогона (любого статуса), по возрастанию id — см. SwarmStore.questionsFor. */
|
|
1280
|
-
questionsFor(run_id) {
|
|
1281
|
-
return this.db.prepare("SELECT * FROM questions WHERE run_id = ? ORDER BY id").all(run_id).map(
|
|
1282
|
-
rowToQuestion
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
/** id прогонов, у которых есть хотя бы один вопрос — доступность «Переиграть» в модалке. */
|
|
1286
|
-
runIdsWithQuestions() {
|
|
1287
|
-
return this.db.prepare("SELECT DISTINCT run_id FROM questions").all().map((r) => r.run_id);
|
|
1288
|
-
}
|
|
1289
|
-
/**
|
|
1290
|
-
* Открытые вопросы дронов отряда Героя #heroId (join questions×runs по hero_run_id).
|
|
1291
|
-
* Герой видит их в squad_status и отвечает через answer_drone; вопрос не из отряда сюда
|
|
1292
|
-
* не попадёт — это и есть гейт «отвечать только своим» (TASWM-81).
|
|
1293
|
-
*/
|
|
1294
|
-
openQuestionsFor(heroId) {
|
|
1295
|
-
return this.db.prepare(
|
|
1296
|
-
"SELECT q.* FROM questions q JOIN runs r ON r.id = q.run_id WHERE r.hero_run_id = ? AND q.status = 'open' ORDER BY q.id"
|
|
1297
|
-
).all(heroId).map(rowToQuestion);
|
|
1298
|
-
}
|
|
1299
|
-
// --- events (лог/хроника) ---
|
|
1300
|
-
addEvent(run_id, kind, text, actor = null) {
|
|
1301
|
-
const clean = sanitizeEventText(text);
|
|
1302
|
-
const insert = () => this.db.prepare("INSERT INTO events (run_id, kind, text, actor) VALUES (?, ?, ?, ?)").run(run_id, kind, clean, actor);
|
|
1303
|
-
try {
|
|
1304
|
-
insert();
|
|
1305
|
-
} catch (err) {
|
|
1306
|
-
if (run_id != null && isForeignKeyError(err)) {
|
|
1307
|
-
console.error(
|
|
1308
|
-
`[swarm-db] FOREIGN KEY constraint failed: \u0441\u043E\u0431\u044B\u0442\u0438\u0435 "${kind}" \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run_id} \u2014 \u0441\u0442\u0440\u043E\u043A\u0438 \u043D\u0435\u0442 \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u043C \u0441\u0442\u043E\u0440\u0435 (\u0441\u043C. TASWM-263: \u0434\u0435\u043C\u043E\u043D \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u043D\u0435 \u0437\u0430\u0432\u043E\u0434\u0438\u0442 \u0435\u0451 \u0437\u0434\u0435\u0441\u044C). \u0417\u0430\u0432\u043E\u0436\u0443 \u0441\u0442\u0430\u0431 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E.`
|
|
1434
|
+
/** ВСЕ вопросы прогона (любого статуса), по возрастанию id — см. SwarmStore.questionsFor. */
|
|
1435
|
+
questionsFor(run_id) {
|
|
1436
|
+
return this.db.prepare("SELECT * FROM questions WHERE run_id = ? ORDER BY id").all(run_id).map(
|
|
1437
|
+
rowToQuestion
|
|
1309
1438
|
);
|
|
1310
|
-
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1311
|
-
insert();
|
|
1312
|
-
return;
|
|
1313
1439
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1440
|
+
/** id прогонов, у которых есть хотя бы один вопрос — доступность «Переиграть» в модалке. */
|
|
1441
|
+
runIdsWithQuestions() {
|
|
1442
|
+
return this.db.prepare("SELECT DISTINCT run_id FROM questions").all().map((r) => r.run_id);
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Открытые вопросы дронов отряда Героя #heroId (join questions×runs по hero_run_id).
|
|
1446
|
+
* Герой видит их в squad_status и отвечает через answer_drone; вопрос не из отряда сюда
|
|
1447
|
+
* не попадёт — это и есть гейт «отвечать только своим» (TASWM-81).
|
|
1448
|
+
*/
|
|
1449
|
+
openQuestionsFor(heroId) {
|
|
1450
|
+
return this.db.prepare(
|
|
1451
|
+
"SELECT q.* FROM questions q JOIN runs r ON r.id = q.run_id WHERE r.hero_run_id = ? AND q.status = 'open' ORDER BY q.id"
|
|
1452
|
+
).all(heroId).map(rowToQuestion);
|
|
1453
|
+
}
|
|
1454
|
+
// --- events (лог/хроника) ---
|
|
1455
|
+
addEvent(run_id, kind, text, actor = null) {
|
|
1456
|
+
const clean = sanitizeEventText(text);
|
|
1457
|
+
const insert = () => this.db.prepare("INSERT INTO events (run_id, kind, text, actor) VALUES (?, ?, ?, ?)").run(run_id, kind, clean, actor);
|
|
1458
|
+
try {
|
|
1459
|
+
insert();
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
if (run_id != null && isForeignKeyError(err)) {
|
|
1462
|
+
console.error(
|
|
1463
|
+
`[swarm-db] FOREIGN KEY constraint failed: \u0441\u043E\u0431\u044B\u0442\u0438\u0435 "${kind}" \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run_id} \u2014 \u0441\u0442\u0440\u043E\u043A\u0438 \u043D\u0435\u0442 \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u043C \u0441\u0442\u043E\u0440\u0435 (\u0441\u043C. TASWM-263: \u0434\u0435\u043C\u043E\u043D \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u043D\u0435 \u0437\u0430\u0432\u043E\u0434\u0438\u0442 \u0435\u0451 \u0437\u0434\u0435\u0441\u044C). \u0417\u0430\u0432\u043E\u0436\u0443 \u0441\u0442\u0430\u0431 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E.`
|
|
1464
|
+
);
|
|
1465
|
+
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1466
|
+
insert();
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
throw err;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
events(run_id) {
|
|
1473
|
+
return this.db.prepare("SELECT * FROM events WHERE run_id = ? ORDER BY id").all(run_id);
|
|
1474
|
+
}
|
|
1475
|
+
/** Хроника роя (вкладка История): Q&A (вопрос+варианты+ответ+автор) + вехи прогонов, новые сверху. */
|
|
1476
|
+
history(limit = 50) {
|
|
1477
|
+
const items = [];
|
|
1478
|
+
const name = (row) => row.name ?? `#${row.run_id}`;
|
|
1479
|
+
for (const row of this.db.prepare(
|
|
1480
|
+
"SELECT q.*, r.task_slug slug, r.area area, r.name name, r.repo repo, r.task_title title, r.pr_url pr, r.task_url taskUrl FROM questions q LEFT JOIN runs r ON r.id = q.run_id"
|
|
1481
|
+
).all()) {
|
|
1482
|
+
const q = rowToQuestion(row);
|
|
1483
|
+
items.push({
|
|
1484
|
+
kind: "qa",
|
|
1485
|
+
at: row.answered_at ?? q.asked_at,
|
|
1486
|
+
slug: row.slug ?? "?",
|
|
1487
|
+
name: name(row),
|
|
1488
|
+
repo: row.repo ?? "",
|
|
1489
|
+
area: row.area ?? null,
|
|
1490
|
+
qid: q.id,
|
|
1491
|
+
question: q.text,
|
|
1492
|
+
options: q.options,
|
|
1493
|
+
answer: q.answer,
|
|
1494
|
+
answered_by: q.answered_by,
|
|
1495
|
+
escalated: q.escalated,
|
|
1496
|
+
open: q.status === "open",
|
|
1497
|
+
title: row.title ?? null,
|
|
1498
|
+
pr: row.pr ?? null,
|
|
1499
|
+
taskUrl: row.taskUrl ?? null
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
for (const row of this.db.prepare(
|
|
1503
|
+
"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"
|
|
1504
|
+
).all()) {
|
|
1505
|
+
if (!isMilestoneKind(row.kind)) continue;
|
|
1506
|
+
const global = row.run_id == null;
|
|
1507
|
+
items.push({
|
|
1508
|
+
kind: "milestone",
|
|
1509
|
+
at: row.at,
|
|
1510
|
+
slug: row.slug ?? (global ? "\u0440\u043E\u0439" : "?"),
|
|
1511
|
+
name: global ? "\u0440\u043E\u0439" : name(row),
|
|
1512
|
+
repo: row.repo ?? "",
|
|
1513
|
+
event: row.kind,
|
|
1514
|
+
text: row.text ?? null,
|
|
1515
|
+
actor: row.actor ?? null,
|
|
1516
|
+
title: row.title ?? null,
|
|
1517
|
+
pr: row.pr ?? null,
|
|
1518
|
+
taskUrl: row.taskUrl ?? null
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
items.sort((a, b) => a.at < b.at ? 1 : a.at > b.at ? -1 : 0);
|
|
1522
|
+
return items.slice(0, limit);
|
|
1523
|
+
}
|
|
1524
|
+
/** Сколько PR смержено сегодня (UTC) — для метрики «Смержено сегодня» на Главной. */
|
|
1525
|
+
mergedToday() {
|
|
1526
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM events WHERE kind = 'merged' AND date(at) = date('now')").get();
|
|
1527
|
+
return row.n;
|
|
1528
|
+
}
|
|
1529
|
+
// --- штормовой режим (TASWM-23): состояние роя в событиях (source of truth) ---
|
|
1530
|
+
/**
|
|
1531
|
+
* В шторме ли рой сейчас: смотрим последнее ГЛОБАЛЬНОЕ (run_id NULL) событие
|
|
1532
|
+
* storm_start/storm_end. storm_start без последующего storm_end → шторм активен.
|
|
1533
|
+
* Выводится из событий → переживает рестарт overmind.
|
|
1534
|
+
*/
|
|
1535
|
+
stormActive() {
|
|
1536
|
+
const row = this.db.prepare("SELECT kind FROM events WHERE run_id IS NULL AND kind IN ('storm_start','storm_end') ORDER BY id DESC LIMIT 1").get();
|
|
1537
|
+
return row?.kind === "storm_start";
|
|
1538
|
+
}
|
|
1539
|
+
/** Время начала ТЕКУЩЕГО шторма (at события storm_start) или null, если шторма нет. */
|
|
1540
|
+
currentStormStartedAt() {
|
|
1541
|
+
const row = this.db.prepare("SELECT kind, at FROM events WHERE run_id IS NULL AND kind IN ('storm_start','storm_end') ORDER BY id DESC LIMIT 1").get();
|
|
1542
|
+
return row?.kind === "storm_start" ? row.at : null;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Время ПОСЛЕДНЕГО отбоя (at события storm_end) или null, если шторм сейчас идёт /
|
|
1546
|
+
* отбоя ещё не было. Для гистерезиса TASWM-208: свежий отбой держит кулдаун на
|
|
1547
|
+
* эвристический перезапуск. Смотрим то же последнее storm_start/storm_end, что и
|
|
1548
|
+
* stormActive/currentStormStartedAt — probe-события не участвуют.
|
|
1549
|
+
*/
|
|
1550
|
+
lastStormEndedAt() {
|
|
1551
|
+
const row = this.db.prepare("SELECT kind, at FROM events WHERE run_id IS NULL AND kind IN ('storm_start','storm_end') ORDER BY id DESC LIMIT 1").get();
|
|
1552
|
+
return row?.kind === "storm_end" ? row.at : null;
|
|
1553
|
+
}
|
|
1554
|
+
/** Пометить вход в шторм (глобальное событие). Идемпотентность — на стороне вызывающего. */
|
|
1555
|
+
startStorm(reason) {
|
|
1556
|
+
this.addEvent(null, "storm_start", reason);
|
|
1557
|
+
}
|
|
1558
|
+
/** Пометить отбой шторма (глобальное событие). */
|
|
1559
|
+
endStorm(reason) {
|
|
1560
|
+
this.addEvent(null, "storm_end", reason);
|
|
1561
|
+
}
|
|
1562
|
+
/** Записать смерть работающего дрона — семпл для скользящего окна детекции шторма. */
|
|
1563
|
+
recordDeath(runId, note) {
|
|
1564
|
+
this.addEvent(runId, "died", note ?? "\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 (dead pid)");
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Смертей дронов (событий died) с момента cutoff. cutoffSeconds — epoch-СЕКУНДЫ;
|
|
1568
|
+
* datetime(?, 'unixepoch') форматирует его тем же «YYYY-MM-DD HH:MM:SS» UTC, что и
|
|
1569
|
+
* datetime('now') в at — строковое сравнение корректно, без парса на стороне JS.
|
|
1570
|
+
*/
|
|
1571
|
+
deathCountSince(cutoffSeconds) {
|
|
1572
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM events WHERE kind='died' AND at >= datetime(?, 'unixepoch')").get(cutoffSeconds);
|
|
1573
|
+
return row.n;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Спавн-фейлов (событий failed на НЕ-резолверных/НЕ-ревьюерных прогонах) с момента
|
|
1577
|
+
* cutoff. Резолверные (merge-main-in) и ревьюерные (авто-ревью, TASWM-148) failed не
|
|
1578
|
+
* считаем штормовым сигналом.
|
|
1579
|
+
*/
|
|
1580
|
+
spawnFailCountSince(cutoffSeconds) {
|
|
1581
|
+
const row = this.db.prepare(
|
|
1582
|
+
"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')"
|
|
1583
|
+
).get(cutoffSeconds);
|
|
1584
|
+
return row.n;
|
|
1585
|
+
}
|
|
1586
|
+
/** Записать результат периодической проверки шторма (health-probe + внешний сигнал). */
|
|
1587
|
+
recordStormProbe(probe, ext, reason, resetHint) {
|
|
1588
|
+
this.addEvent(null, "storm_probe", encodeProbe({ probe, ext, reason: reason ?? null, resetHint: resetHint ?? null }));
|
|
1589
|
+
}
|
|
1590
|
+
/** Последняя запись storm_probe (для троттлинга по времени и последнего внешнего сигнала). */
|
|
1591
|
+
latestStormProbe() {
|
|
1592
|
+
const row = this.db.prepare("SELECT text, at FROM events WHERE run_id IS NULL AND kind='storm_probe' ORDER BY id DESC LIMIT 1").get();
|
|
1593
|
+
return row ? { rec: decodeProbe(row.text), at: row.at } : null;
|
|
1594
|
+
}
|
|
1595
|
+
/** Записи storm_probe текущего шторма (at >= sinceIso), по возрастанию — для счётчика успехов подряд. */
|
|
1596
|
+
stormProbeRecordsSince(sinceIso) {
|
|
1597
|
+
const rows = this.db.prepare("SELECT text FROM events WHERE run_id IS NULL AND kind='storm_probe' AND at >= ? ORDER BY id").all(sinceIso);
|
|
1598
|
+
return rows.map((r) => decodeProbe(r.text));
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Последняя РЕАЛЬНАЯ health-probe (probe !== null; ext-only записи пропускаем) с её
|
|
1602
|
+
* временем. TASWM-137 п.д: подтверждающая проба перед входом в шторм по эвристике —
|
|
1603
|
+
* зелёная проба вето́ит ложный шторм; свежую (в пределах probeIntervalMs) переиспользуем,
|
|
1604
|
+
* не гоняя новую каждый тик. Смотрим только хвост (LIMIT) — старее нам всё равно не нужно.
|
|
1605
|
+
*/
|
|
1606
|
+
lastHealthProbe() {
|
|
1607
|
+
const rows = this.db.prepare("SELECT text, at FROM events WHERE run_id IS NULL AND kind='storm_probe' ORDER BY id DESC LIMIT 20").all();
|
|
1608
|
+
for (const r of rows) {
|
|
1609
|
+
const rec = decodeProbe(r.text);
|
|
1610
|
+
if (rec.probe !== null) return { ok: rec.probe, at: r.at };
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1458
1615
|
}
|
|
1459
|
-
};
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
// src/cli.ts
|
|
1619
|
+
import { basename as basename4 } from "path";
|
|
1620
|
+
import { existsSync as existsSync9, readdirSync as readdirSync6, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
1621
|
+
import { createInterface } from "readline/promises";
|
|
1622
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
1460
1623
|
|
|
1461
1624
|
// src/swarmClient.ts
|
|
1462
|
-
|
|
1625
|
+
init_config();
|
|
1626
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1463
1627
|
import { dirname } from "path";
|
|
1464
1628
|
import { randomUUID } from "crypto";
|
|
1465
1629
|
|
|
@@ -1478,6 +1642,7 @@ function execQuiet(file, args = [], opts = {}) {
|
|
|
1478
1642
|
var PROC_SNAPSHOT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
1479
1643
|
|
|
1480
1644
|
// src/resources.ts
|
|
1645
|
+
init_config();
|
|
1481
1646
|
import { readFileSync as readFileSync2, statfsSync } from "fs";
|
|
1482
1647
|
function isTransientResourceError(e) {
|
|
1483
1648
|
const code = e?.code;
|
|
@@ -1619,13 +1784,18 @@ function hostResourceSnapshot() {
|
|
|
1619
1784
|
};
|
|
1620
1785
|
}
|
|
1621
1786
|
|
|
1787
|
+
// src/swarmClient.ts
|
|
1788
|
+
init_names2();
|
|
1789
|
+
init_storm();
|
|
1790
|
+
|
|
1622
1791
|
// src/logTail.ts
|
|
1623
|
-
import { closeSync, existsSync
|
|
1792
|
+
import { closeSync, existsSync, openSync, readSync, readdirSync, statSync } from "fs";
|
|
1624
1793
|
import { join as join2 } from "path";
|
|
1625
1794
|
var TAIL_LINES_FALLBACK = 200;
|
|
1626
1795
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
1627
1796
|
|
|
1628
1797
|
// src/swarmClient.ts
|
|
1798
|
+
init_src();
|
|
1629
1799
|
var RESOLVER_TERMINAL = /* @__PURE__ */ new Set([...TERMINAL_STATUSES, "stalled"]);
|
|
1630
1800
|
var SLUG_TERMINAL = new Set(TERMINAL_STATUSES);
|
|
1631
1801
|
function runRef(runKey) {
|
|
@@ -1634,6 +1804,11 @@ function runRef(runKey) {
|
|
|
1634
1804
|
function isRunRef(v) {
|
|
1635
1805
|
return typeof v === "object" && v !== null && "__runRef" in v;
|
|
1636
1806
|
}
|
|
1807
|
+
function isHostCommandShape(v) {
|
|
1808
|
+
const c = v;
|
|
1809
|
+
return !!c && typeof c === "object" && typeof c.id === "string" && !!c.id && typeof c.kind === "string";
|
|
1810
|
+
}
|
|
1811
|
+
var EMPTY_HEARTBEAT_REPLY = { commands: [], logRequests: [] };
|
|
1637
1812
|
function jsonFromTextContent(res) {
|
|
1638
1813
|
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
1639
1814
|
if (text) {
|
|
@@ -1721,6 +1896,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
1721
1896
|
flushing = false;
|
|
1722
1897
|
flushTimer = null;
|
|
1723
1898
|
backoffMs = 500;
|
|
1899
|
+
/** Сколько раз пробуем отвергнутую запись, прежде чем выбросить её (TASWM-338). */
|
|
1900
|
+
rejected = /* @__PURE__ */ new Map();
|
|
1901
|
+
static REJECT_LIMIT = 5;
|
|
1724
1902
|
static BACKOFF_BASE = 500;
|
|
1725
1903
|
static BACKOFF_MAX = 3e4;
|
|
1726
1904
|
constructor(opts) {
|
|
@@ -1732,7 +1910,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1732
1910
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
1733
1911
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
1734
1912
|
this.host = opts.host ?? null;
|
|
1735
|
-
this.repoExists = opts.repoExists ??
|
|
1913
|
+
this.repoExists = opts.repoExists ?? existsSync2;
|
|
1736
1914
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
1737
1915
|
this.outboxPath = opts.outboxPath;
|
|
1738
1916
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
@@ -1742,14 +1920,14 @@ var SwarmClient = class _SwarmClient {
|
|
|
1742
1920
|
}
|
|
1743
1921
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
1744
1922
|
recover() {
|
|
1745
|
-
if (
|
|
1923
|
+
if (existsSync2(this.mapPath)) {
|
|
1746
1924
|
try {
|
|
1747
1925
|
const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
|
|
1748
1926
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
1749
1927
|
} catch {
|
|
1750
1928
|
}
|
|
1751
1929
|
}
|
|
1752
|
-
if (
|
|
1930
|
+
if (existsSync2(this.outboxPath)) {
|
|
1753
1931
|
const txt = readFileSync3(this.outboxPath, "utf8");
|
|
1754
1932
|
for (const line of txt.split("\n")) {
|
|
1755
1933
|
const s = line.trim();
|
|
@@ -1770,7 +1948,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1770
1948
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
1771
1949
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
1772
1950
|
replayGlobalEvents() {
|
|
1773
|
-
if (!
|
|
1951
|
+
if (!existsSync2(this.stormPath)) return;
|
|
1774
1952
|
let txt;
|
|
1775
1953
|
try {
|
|
1776
1954
|
txt = readFileSync3(this.stormPath, "utf8");
|
|
@@ -2441,7 +2619,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
2441
2619
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
2442
2620
|
*/
|
|
2443
2621
|
reconcileRuntime(opts) {
|
|
2444
|
-
const wtExists = opts.worktreeExists ??
|
|
2622
|
+
const wtExists = opts.worktreeExists ?? existsSync2;
|
|
2445
2623
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
2446
2624
|
try {
|
|
2447
2625
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -2802,17 +2980,24 @@ var SwarmClient = class _SwarmClient {
|
|
|
2802
2980
|
* прислал предыдущий (более свежий по коду) процесс демона.
|
|
2803
2981
|
*/
|
|
2804
2982
|
/**
|
|
2805
|
-
* TASWM-
|
|
2806
|
-
*
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
2809
|
-
|
|
2810
|
-
|
|
2983
|
+
* TASWM-321: ВОЗВРАЩАЕТ команды этой машине (пауза/рестарт/обновление/…), приехавшие
|
|
2984
|
+
* в ответе на пульс. Это единственный момент, когда прод может что-то сказать демону:
|
|
2985
|
+
* тот живёт за домашним роутером без белого адреса, и связь всегда инициирует он сам.
|
|
2986
|
+
* Прод, отдав команды, метит их доставленными — повторный пульс их не вернёт, поэтому
|
|
2987
|
+
* потерять ответ здесь значит потерять команду; парсим бережно и не бросаем.
|
|
2988
|
+
*
|
|
2989
|
+
* TASWM-322: тем же ответом приезжают запросы «пришли хвост лога» (logRequests) —
|
|
2990
|
+
* канал «прод → демон» один на оба вида поручений, поэтому и разбираем их вместе.
|
|
2991
|
+
* Возвращаем ОБА списка объектом: два разных потребителя в overmind, и слепить их в
|
|
2992
|
+
* один список значило бы заставить каждого фильтровать чужое.
|
|
2993
|
+
*
|
|
2811
2994
|
* TASWM-326: тем же пульсом уходит ПАУЗА этой машины (`pause` — durable-запись из
|
|
2812
2995
|
* pause.json либо null). В парке машин «на паузе» обязано отличаться от «нет работы»
|
|
2813
2996
|
* и от «молчит», а единственный канал «демон → прод» с нужной кадансностью — пульс.
|
|
2814
2997
|
* Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
|
|
2815
2998
|
* означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
|
|
2999
|
+
*
|
|
3000
|
+
* Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
|
|
2816
3001
|
*/
|
|
2817
3002
|
async heartbeat(daemonVersion, pause) {
|
|
2818
3003
|
try {
|
|
@@ -2827,9 +3012,33 @@ var SwarmClient = class _SwarmClient {
|
|
|
2827
3012
|
args.hostName = this.host.hostName;
|
|
2828
3013
|
}
|
|
2829
3014
|
const res = await this.call("swarm_heartbeat", args);
|
|
2830
|
-
|
|
3015
|
+
if (res?.isError) return EMPTY_HEARTBEAT_REPLY;
|
|
3016
|
+
const data = res?.structuredContent ?? {};
|
|
3017
|
+
return {
|
|
3018
|
+
commands: Array.isArray(data.commands) ? data.commands.filter(isHostCommandShape) : [],
|
|
3019
|
+
logRequests: parseLogRequests(data.logRequests)
|
|
3020
|
+
};
|
|
3021
|
+
} catch {
|
|
3022
|
+
return EMPTY_HEARTBEAT_REPLY;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
/**
|
|
3026
|
+
* TASWM-321: подтвердить исполнение команды (`swarm_command_ack`). ПРЯМОЙ вызов, не
|
|
3027
|
+
* outbox: подтверждение — «скажи СЕЙЧАС», ради него команду и заводили («без
|
|
3028
|
+
* подтверждения непонятно, обновился он или команда потерялась по дороге»), а
|
|
3029
|
+
* промежуточные шаги обновления, доехавшие после самого обновления, бессмысленны.
|
|
3030
|
+
* Не бросает: сорвавшийся ack не должен рушить тик демона — команда останется в
|
|
3031
|
+
* прошлом состоянии, следующий шаг/финал её всё равно подвинут.
|
|
3032
|
+
*/
|
|
3033
|
+
async ackCommand(commandId, status, patch = {}) {
|
|
3034
|
+
try {
|
|
3035
|
+
await this.call("swarm_command_ack", {
|
|
3036
|
+
commandId,
|
|
3037
|
+
status,
|
|
3038
|
+
...patch.step ? { step: patch.step } : {},
|
|
3039
|
+
...patch.result ? { result: patch.result.slice(0, 2e3) } : {}
|
|
3040
|
+
});
|
|
2831
3041
|
} catch {
|
|
2832
|
-
return [];
|
|
2833
3042
|
}
|
|
2834
3043
|
}
|
|
2835
3044
|
/**
|
|
@@ -3238,7 +3447,23 @@ var SwarmClient = class _SwarmClient {
|
|
|
3238
3447
|
} catch {
|
|
3239
3448
|
break;
|
|
3240
3449
|
}
|
|
3241
|
-
if (res?.isError)
|
|
3450
|
+
if (res?.isError) {
|
|
3451
|
+
const reason = (res.content ?? []).map((c) => c.text).filter(Boolean).join(" ").trim() || "\u0431\u0435\u0437 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u044F";
|
|
3452
|
+
const tries = (this.rejected.get(e.seq) ?? 0) + 1;
|
|
3453
|
+
this.rejected.set(e.seq, tries);
|
|
3454
|
+
if (tries >= _SwarmClient.REJECT_LIMIT) {
|
|
3455
|
+
this.pending.shift();
|
|
3456
|
+
this.rejected.delete(e.seq);
|
|
3457
|
+
console.error(
|
|
3458
|
+
`[swarm-client] \u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433 ${call.tool} ${tries} \u0440\u0430\u0437(\u0430) \u2014 \u0412\u042B\u0411\u0420\u0410\u0421\u042B\u0412\u0410\u042E \u044D\u0442\u0443 \u0437\u0430\u043F\u0438\u0441\u044C, \u0438\u043D\u0430\u0447\u0435 \u043E\u043D\u0430 \u0434\u0435\u0440\u0436\u0438\u0442 \u0432\u0441\u044E \u043E\u0447\u0435\u0440\u0435\u0434\u044C. \u041F\u043E\u0442\u0435\u0440\u044F\u043D\u043E: ${e.t}${e.t === "event" ? ` (${e.kind})` : ""}. \u041F\u0440\u0438\u0447\u0438\u043D\u0430 \u043E\u0442\u043A\u0430\u0437\u0430: ${reason}`
|
|
3459
|
+
);
|
|
3460
|
+
continue;
|
|
3461
|
+
}
|
|
3462
|
+
console.error(
|
|
3463
|
+
`[swarm-client] \u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433 ${call.tool}: ${reason} \u2014 \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${tries}/${_SwarmClient.REJECT_LIMIT}, \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 ${this.pending.length} \u0437\u0430\u043F\u0438\u0441(\u0435\u0439). \u041F\u043E\u043A\u0430 \u043E\u043D\u0430 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0451\u0442, \u043F\u0440\u043E\u0434 \u041D\u0415 \u0443\u0432\u0438\u0434\u0438\u0442 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u043E\u0432\u043E\u0433\u043E.`
|
|
3464
|
+
);
|
|
3465
|
+
break;
|
|
3466
|
+
}
|
|
3242
3467
|
if (e.t === "run") {
|
|
3243
3468
|
const run = res.structuredContent?.run;
|
|
3244
3469
|
if (run?.id && this.prodIdByKey.get(e.runKey) !== run.id) {
|
|
@@ -3298,6 +3523,9 @@ function parseDecision(v) {
|
|
|
3298
3523
|
}
|
|
3299
3524
|
}
|
|
3300
3525
|
|
|
3526
|
+
// src/cli.ts
|
|
3527
|
+
init_config();
|
|
3528
|
+
|
|
3301
3529
|
// src/daemonLaunch.ts
|
|
3302
3530
|
import { openSync as openSync2, closeSync as closeSync2, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
|
|
3303
3531
|
import { spawn } from "child_process";
|
|
@@ -3609,6 +3837,7 @@ async function cleanupWorktreeProcesses(worktree, opts = {}) {
|
|
|
3609
3837
|
}
|
|
3610
3838
|
|
|
3611
3839
|
// src/repoRegistry.ts
|
|
3840
|
+
init_config();
|
|
3612
3841
|
import { $ } from "execa";
|
|
3613
3842
|
function parseRepoFullName(remoteUrl) {
|
|
3614
3843
|
const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
|
|
@@ -3634,14 +3863,14 @@ async function buildRepoRegistry(paths) {
|
|
|
3634
3863
|
|
|
3635
3864
|
// src/hostIdentity.ts
|
|
3636
3865
|
import { hostname } from "os";
|
|
3637
|
-
import { existsSync as
|
|
3866
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
3638
3867
|
import { join as join3 } from "path";
|
|
3639
3868
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3640
3869
|
function hostIdentityPath(home) {
|
|
3641
3870
|
return join3(home, "host.json");
|
|
3642
3871
|
}
|
|
3643
3872
|
function loadHostIdentity(deps) {
|
|
3644
|
-
const exists = deps.exists ??
|
|
3873
|
+
const exists = deps.exists ?? existsSync3;
|
|
3645
3874
|
const readFile2 = deps.readFile ?? ((p) => readFileSync5(p, "utf8"));
|
|
3646
3875
|
const writeFile2 = deps.writeFile ?? ((p, data) => {
|
|
3647
3876
|
writeFileSync4(`${p}.tmp`, data);
|
|
@@ -3671,6 +3900,7 @@ function loadHostIdentity(deps) {
|
|
|
3671
3900
|
}
|
|
3672
3901
|
|
|
3673
3902
|
// src/runList.ts
|
|
3903
|
+
init_src();
|
|
3674
3904
|
function hostGroupHeader(g, thisHostId) {
|
|
3675
3905
|
const running = g.runs.filter((r) => r.status === "running").length;
|
|
3676
3906
|
const mine = !g.unknown && g.key === thisHostId ? " (\u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430)" : "";
|
|
@@ -3702,6 +3932,7 @@ function renderRunList(runs, opts) {
|
|
|
3702
3932
|
}
|
|
3703
3933
|
|
|
3704
3934
|
// src/hostPick.ts
|
|
3935
|
+
init_src();
|
|
3705
3936
|
function chooseSpawnHost(flag) {
|
|
3706
3937
|
if (flag == null) return { kind: "self" };
|
|
3707
3938
|
const ref = normalizeHostRef(flag);
|
|
@@ -3748,6 +3979,7 @@ function hostLines(hosts, opts = {}) {
|
|
|
3748
3979
|
}
|
|
3749
3980
|
|
|
3750
3981
|
// src/web.ts
|
|
3982
|
+
init_src();
|
|
3751
3983
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
3752
3984
|
import { basename as basename2 } from "path";
|
|
3753
3985
|
import { serve as serve2 } from "@hono/node-server";
|
|
@@ -3941,7 +4173,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
3941
4173
|
// package.json
|
|
3942
4174
|
var package_default = {
|
|
3943
4175
|
name: "@taskless-app/swarm",
|
|
3944
|
-
version: "0.4.
|
|
4176
|
+
version: "0.4.10",
|
|
3945
4177
|
type: "module",
|
|
3946
4178
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
3947
4179
|
keywords: [
|
|
@@ -3990,13 +4222,14 @@ var package_default = {
|
|
|
3990
4222
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
3991
4223
|
execa: "^10.0.0",
|
|
3992
4224
|
zod: "3.25.76"
|
|
3993
|
-
},
|
|
3994
|
-
optionalDependencies: {
|
|
3995
|
-
"better-sqlite3": "^12.11.1"
|
|
3996
4225
|
}
|
|
3997
4226
|
};
|
|
3998
4227
|
|
|
4228
|
+
// src/web.ts
|
|
4229
|
+
init_storm();
|
|
4230
|
+
|
|
3999
4231
|
// src/gh.ts
|
|
4232
|
+
init_src();
|
|
4000
4233
|
import { $ as $3 } from "execa";
|
|
4001
4234
|
var NO_CHECKS_STDERR = /no checks reported/i;
|
|
4002
4235
|
var realChecksRunner = async (pr) => {
|
|
@@ -4042,7 +4275,11 @@ async function ghChecks(pr, run = realChecksRunner) {
|
|
|
4042
4275
|
return { bucket, text, tone, total, pass, fail, pending: pending2 };
|
|
4043
4276
|
}
|
|
4044
4277
|
|
|
4278
|
+
// src/web.ts
|
|
4279
|
+
init_config();
|
|
4280
|
+
|
|
4045
4281
|
// src/droneChannel.ts
|
|
4282
|
+
init_src();
|
|
4046
4283
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
4047
4284
|
import { chmodSync, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
4048
4285
|
import { dirname as dirname2 } from "path";
|
|
@@ -4088,6 +4325,7 @@ function verifyLocalCliToken(token, secret) {
|
|
|
4088
4325
|
var EVENT_KIND_SET = new Set(EVENT_KINDS);
|
|
4089
4326
|
|
|
4090
4327
|
// src/oauth.ts
|
|
4328
|
+
init_config();
|
|
4091
4329
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
4092
4330
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync } from "fs";
|
|
4093
4331
|
import { join as join5 } from "path";
|
|
@@ -5786,14 +6024,14 @@ import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDronePro
|
|
|
5786
6024
|
|
|
5787
6025
|
// src/launcher.ts
|
|
5788
6026
|
import { spawn as spawn2 } from "child_process";
|
|
5789
|
-
import { closeSync as closeSync3, existsSync as
|
|
5790
|
-
import { createRequire
|
|
6027
|
+
import { closeSync as closeSync3, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync as openSync3, readdirSync as readdirSync4, statSync as statSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
6028
|
+
import { createRequire } from "module";
|
|
5791
6029
|
import { homedir as homedir2 } from "os";
|
|
5792
6030
|
import { dirname as dirname3, join as join7 } from "path";
|
|
5793
6031
|
import { fileURLToPath } from "url";
|
|
5794
6032
|
|
|
5795
6033
|
// src/claudeBin.ts
|
|
5796
|
-
import { existsSync as
|
|
6034
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
5797
6035
|
import { win32 as winPath } from "path";
|
|
5798
6036
|
var CLAUDE_FALLBACK = "claude";
|
|
5799
6037
|
function compareVersionDirs(a, b) {
|
|
@@ -5830,7 +6068,7 @@ function winVersionRoots(env, listDir) {
|
|
|
5830
6068
|
function findWindowsClaude(opts = {}) {
|
|
5831
6069
|
const env = opts.env ?? process.env;
|
|
5832
6070
|
const listDir = opts.listDir ?? realListDir;
|
|
5833
|
-
const fileExists = opts.fileExists ??
|
|
6071
|
+
const fileExists = opts.fileExists ?? existsSync4;
|
|
5834
6072
|
let best = null;
|
|
5835
6073
|
for (const root of winVersionRoots(env, listDir)) {
|
|
5836
6074
|
for (const version of listDir(root)) {
|
|
@@ -5984,7 +6222,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
|
|
|
5984
6222
|
}
|
|
5985
6223
|
|
|
5986
6224
|
// src/launcher.ts
|
|
5987
|
-
var requireFromHere =
|
|
6225
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
5988
6226
|
function resolveTsxCli() {
|
|
5989
6227
|
try {
|
|
5990
6228
|
return requireFromHere.resolve("tsx/cli");
|
|
@@ -6050,7 +6288,7 @@ function roleDir(worktree, kind) {
|
|
|
6050
6288
|
return join7(worktree, ".claude", "roles", kind);
|
|
6051
6289
|
}
|
|
6052
6290
|
function roleDirs(worktree, kind) {
|
|
6053
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
6291
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync5(dir));
|
|
6054
6292
|
}
|
|
6055
6293
|
function nextRunLogPath(runLogsDir, runId) {
|
|
6056
6294
|
const dir = join7(runLogsDir, String(runId));
|
|
@@ -6146,7 +6384,7 @@ function makeLauncher(cfg) {
|
|
|
6146
6384
|
const realHome = cfg.droneIsolation?.realHome ?? homedir2();
|
|
6147
6385
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
6148
6386
|
const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
|
|
6149
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !
|
|
6387
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync5(join7(cfg.droneIsolation.swarmHome, "token"))) {
|
|
6150
6388
|
console.error(
|
|
6151
6389
|
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join7(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
|
|
6152
6390
|
);
|
|
@@ -6179,7 +6417,7 @@ function makeLauncher(cfg) {
|
|
|
6179
6417
|
}
|
|
6180
6418
|
|
|
6181
6419
|
// src/git.ts
|
|
6182
|
-
import { existsSync as
|
|
6420
|
+
import { existsSync as existsSync6, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
6183
6421
|
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
6184
6422
|
import { isAbsolute, join as join8 } from "path";
|
|
6185
6423
|
import { $ as $4 } from "execa";
|
|
@@ -6197,7 +6435,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
6197
6435
|
}
|
|
6198
6436
|
}
|
|
6199
6437
|
async function ensureWorktreeDirGone(worktree) {
|
|
6200
|
-
if (!
|
|
6438
|
+
if (!existsSync6(worktree)) return false;
|
|
6201
6439
|
if (!isLinkedWorktree(worktree)) {
|
|
6202
6440
|
console.error(
|
|
6203
6441
|
`[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
|
|
@@ -6212,7 +6450,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
6212
6450
|
);
|
|
6213
6451
|
return true;
|
|
6214
6452
|
}
|
|
6215
|
-
if (
|
|
6453
|
+
if (existsSync6(worktree)) {
|
|
6216
6454
|
console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
|
|
6217
6455
|
}
|
|
6218
6456
|
return true;
|
|
@@ -6225,6 +6463,7 @@ async function removeWorktree(repo, path) {
|
|
|
6225
6463
|
}
|
|
6226
6464
|
|
|
6227
6465
|
// src/agentToken.ts
|
|
6466
|
+
init_config();
|
|
6228
6467
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
6229
6468
|
import { join as join9 } from "path";
|
|
6230
6469
|
var STORE_PATH2 = join9(SWARM_HOME, "agent.json");
|
|
@@ -6271,6 +6510,7 @@ async function probeAgentToken(args, fetchFn = fetch) {
|
|
|
6271
6510
|
}
|
|
6272
6511
|
|
|
6273
6512
|
// src/taskless.ts
|
|
6513
|
+
init_src();
|
|
6274
6514
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6275
6515
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6276
6516
|
|
|
@@ -6289,7 +6529,11 @@ import { readFile as fsReadFile } from "fs/promises";
|
|
|
6289
6529
|
import { basename as basename3, isAbsolute as isAbsolute2, join as join11 } from "path";
|
|
6290
6530
|
var SHOT_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
6291
6531
|
|
|
6532
|
+
// src/taskless.ts
|
|
6533
|
+
init_config();
|
|
6534
|
+
|
|
6292
6535
|
// src/workspace.ts
|
|
6536
|
+
init_config();
|
|
6293
6537
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3 } from "fs";
|
|
6294
6538
|
import { join as join12 } from "path";
|
|
6295
6539
|
var WORKSPACE_PATH = join12(SWARM_HOME, "workspace.json");
|
|
@@ -6644,6 +6888,7 @@ async function launchChromium(opts = {}) {
|
|
|
6644
6888
|
}
|
|
6645
6889
|
|
|
6646
6890
|
// src/shotCli.ts
|
|
6891
|
+
init_config();
|
|
6647
6892
|
function isFlagValue(v) {
|
|
6648
6893
|
return v !== void 0 && !v.startsWith("--");
|
|
6649
6894
|
}
|
|
@@ -6718,6 +6963,7 @@ async function runShotCli(argv, deps = {}) {
|
|
|
6718
6963
|
}
|
|
6719
6964
|
|
|
6720
6965
|
// src/daemonLink.ts
|
|
6966
|
+
init_config();
|
|
6721
6967
|
var TIMEOUT_MS = 5e3;
|
|
6722
6968
|
var ACTION_TIMEOUT_MS = 2e4;
|
|
6723
6969
|
var BASE = `http://127.0.0.1:${WEB_PORT}`;
|
|
@@ -6783,14 +7029,14 @@ async function daemonStatus2() {
|
|
|
6783
7029
|
var NO_DAEMON_NOTE = `\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 (\u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT}) \u2014 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u044E \u041B\u041E\u041A\u0410\u041B\u042C\u041D\u042B\u0419 \u0441\u0442\u043E\u0440 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u044B, \u043E\u043D \u043C\u043E\u0436\u0435\u0442 \u0440\u0430\u0441\u0445\u043E\u0434\u0438\u0442\u044C\u0441\u044F \u0441 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u043E\u043C. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0434\u0435\u043C\u043E\u043D\u0430: swarm start`;
|
|
6784
7030
|
|
|
6785
7031
|
// src/pauseState.ts
|
|
6786
|
-
import { existsSync as
|
|
7032
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
6787
7033
|
import { join as join13 } from "path";
|
|
6788
7034
|
function pauseStatePath(home) {
|
|
6789
7035
|
return join13(home, "pause.json");
|
|
6790
7036
|
}
|
|
6791
7037
|
var ACTIVE = { paused: false, at: "", source: "cli" };
|
|
6792
7038
|
function loadPauseState(home, io = {}) {
|
|
6793
|
-
const exists = io.exists ??
|
|
7039
|
+
const exists = io.exists ?? existsSync7;
|
|
6794
7040
|
const readFile2 = io.readFile ?? ((p) => readFileSync10(p, "utf8"));
|
|
6795
7041
|
const path = pauseStatePath(home);
|
|
6796
7042
|
if (!exists(path)) return ACTIVE;
|
|
@@ -6810,7 +7056,7 @@ function loadPauseState(home, io = {}) {
|
|
|
6810
7056
|
function savePauseState(home, state, io = {}) {
|
|
6811
7057
|
const path = pauseStatePath(home);
|
|
6812
7058
|
if (!state.paused) {
|
|
6813
|
-
const remove = io.remove ?? ((p) =>
|
|
7059
|
+
const remove = io.remove ?? ((p) => existsSync7(p) && unlinkSync4(p));
|
|
6814
7060
|
remove(path);
|
|
6815
7061
|
return;
|
|
6816
7062
|
}
|
|
@@ -6856,6 +7102,7 @@ function readHeartbeat(path) {
|
|
|
6856
7102
|
}
|
|
6857
7103
|
|
|
6858
7104
|
// src/cli.ts
|
|
7105
|
+
init_src();
|
|
6859
7106
|
var PACKAGE_VERSION2 = package_default.version;
|
|
6860
7107
|
function takeRepo(argv) {
|
|
6861
7108
|
const i = argv.indexOf("--repo");
|
|
@@ -7353,7 +7600,10 @@ function effectiveWorktreesDir() {
|
|
|
7353
7600
|
return effectiveWorktreesDirMemo;
|
|
7354
7601
|
}
|
|
7355
7602
|
async function makeCliStore(repo) {
|
|
7356
|
-
if (SWARM_BACKEND !== "client")
|
|
7603
|
+
if (SWARM_BACKEND !== "client") {
|
|
7604
|
+
const { SwarmDb: SwarmDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
7605
|
+
return new SwarmDb2(DB_PATH, repo);
|
|
7606
|
+
}
|
|
7357
7607
|
const call = resilientSwarmMcp({
|
|
7358
7608
|
onDown: (ms) => console.warn(`Swarm MCP \u043C\u043E\u043B\u0447\u0438\u0442 ${Math.round(ms / 1e3)}\u0441 \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u043C \u0432\u0441\u043B\u0435\u043F\u0443\u044E, \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0434\u043E\u043C.`),
|
|
7359
7609
|
onUp: () => console.warn("Swarm MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
|
|
@@ -7411,7 +7661,8 @@ if (cmd === "import-db") {
|
|
|
7411
7661
|
console.error("\u043D\u0443\u0436\u043D\u043E: swarm import-db <path> --repo <path>");
|
|
7412
7662
|
process.exit(1);
|
|
7413
7663
|
}
|
|
7414
|
-
const
|
|
7664
|
+
const { SwarmDb: SwarmDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
7665
|
+
const sdb = new SwarmDb2(DB_PATH, repo);
|
|
7415
7666
|
const res = sdb.importDb(path, repo);
|
|
7416
7667
|
if (res.imported) console.log(`\u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E ${res.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0438\u0437 ${path} (repo=${repo})`);
|
|
7417
7668
|
else console.log(`${path} \u0443\u0436\u0435 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D \u0440\u0430\u043D\u0435\u0435 (${res.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432) \u2014 no-op`);
|