@taskless-app/swarm 0.4.5 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/cli.js +1181 -986
- package/dist/overmind.js +2311 -2127
- 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,44 @@ 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
374
|
return cleaned.length ? cleaned : null;
|
|
340
375
|
}
|
|
376
|
+
var ANSI_CSI_RE, CONTROL_CHARS_RE;
|
|
377
|
+
var init_text = __esm({
|
|
378
|
+
"../swarm-core/src/text.ts"() {
|
|
379
|
+
"use strict";
|
|
380
|
+
ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
381
|
+
CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ../swarm-core/src/comment.ts
|
|
386
|
+
var init_comment = __esm({
|
|
387
|
+
"../swarm-core/src/comment.ts"() {
|
|
388
|
+
"use strict";
|
|
389
|
+
}
|
|
390
|
+
});
|
|
341
391
|
|
|
342
392
|
// ../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
393
|
function toRoman(n) {
|
|
373
394
|
if (n < 1 || n > 3999) return String(n);
|
|
374
395
|
const table = [
|
|
@@ -396,10 +417,43 @@ function genName(base3, g) {
|
|
|
396
417
|
function baseName(name) {
|
|
397
418
|
return name.split(" ")[0];
|
|
398
419
|
}
|
|
420
|
+
var DRONE_POOL, ACTIVE_NAME_STATUSES;
|
|
421
|
+
var init_names = __esm({
|
|
422
|
+
"../swarm-core/src/names.ts"() {
|
|
423
|
+
"use strict";
|
|
424
|
+
DRONE_POOL = [
|
|
425
|
+
// Латинский бестиарий (15)
|
|
426
|
+
"SCARAB",
|
|
427
|
+
"VESPA",
|
|
428
|
+
"MANTIS",
|
|
429
|
+
"HORNET",
|
|
430
|
+
"CICADA",
|
|
431
|
+
"LOCUST",
|
|
432
|
+
"ATTA",
|
|
433
|
+
"FORMICA",
|
|
434
|
+
"MYRMEX",
|
|
435
|
+
"APIS",
|
|
436
|
+
"TERMES",
|
|
437
|
+
"GRYLLUS",
|
|
438
|
+
"LAMPYRIS",
|
|
439
|
+
"ARACHNE",
|
|
440
|
+
"SPHINX",
|
|
441
|
+
// Мифо-стаи (9)
|
|
442
|
+
"TIAMAT",
|
|
443
|
+
"JORMUNGAND",
|
|
444
|
+
"GRENDEL",
|
|
445
|
+
"TYPHON",
|
|
446
|
+
"FENRIS",
|
|
447
|
+
"GARM",
|
|
448
|
+
"KUKULKAN",
|
|
449
|
+
"LEVIATHAN",
|
|
450
|
+
"SURTUR"
|
|
451
|
+
];
|
|
452
|
+
ACTIVE_NAME_STATUSES = ["queued", "running", "blocked", "review"];
|
|
453
|
+
}
|
|
454
|
+
});
|
|
399
455
|
|
|
400
456
|
// ../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
457
|
function normalizeBaseBranch(raw) {
|
|
404
458
|
if (raw == null) return void 0;
|
|
405
459
|
if (typeof raw !== "string") return null;
|
|
@@ -409,19 +463,22 @@ function normalizeBaseBranch(raw) {
|
|
|
409
463
|
if (v.includes("..") || v.endsWith("/") || v.endsWith(".lock")) return null;
|
|
410
464
|
return BASE_BRANCH_RE.test(v) ? v : null;
|
|
411
465
|
}
|
|
466
|
+
var BASE_BRANCH_MAX, BASE_BRANCH_RE;
|
|
467
|
+
var init_baseBranch = __esm({
|
|
468
|
+
"../swarm-core/src/baseBranch.ts"() {
|
|
469
|
+
"use strict";
|
|
470
|
+
BASE_BRANCH_MAX = 255;
|
|
471
|
+
BASE_BRANCH_RE = /^[A-Za-z0-9._][A-Za-z0-9._/-]*$/;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
412
474
|
|
|
413
475
|
// ../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
476
|
function normalizeHostRef(value) {
|
|
416
477
|
if (value == null) return null;
|
|
417
478
|
const trimmed = value.trim();
|
|
418
479
|
if (!trimmed) return null;
|
|
419
480
|
return ANY_HOST_TOKENS.has(trimmed.toLowerCase()) ? null : trimmed;
|
|
420
481
|
}
|
|
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
482
|
function splitQueue(items, ref) {
|
|
426
483
|
const queue = [];
|
|
427
484
|
const hosted = [];
|
|
@@ -432,7 +489,6 @@ function splitQueue(items, ref) {
|
|
|
432
489
|
}
|
|
433
490
|
return { queue, hosted };
|
|
434
491
|
}
|
|
435
|
-
var HOST_HEARTBEAT_STALE_MS = 45e3;
|
|
436
492
|
function groupByHost(items, ref, opts = {}) {
|
|
437
493
|
const { hosts = [], nowMs = Date.now(), staleMs = HOST_HEARTBEAT_STALE_MS } = opts;
|
|
438
494
|
const infoById = new Map(hosts.map((h) => [h.hostId, h]));
|
|
@@ -488,10 +544,51 @@ function hostStateText(host, nowMs = Date.now(), staleMs = HOST_HEARTBEAT_STALE_
|
|
|
488
544
|
function shouldGroupByHost(groups) {
|
|
489
545
|
return groups.length > 1;
|
|
490
546
|
}
|
|
547
|
+
var ANY_HOST_TOKENS, UNKNOWN_HOST_KEY, UNKNOWN_HOST_LABEL, QUEUE_LABEL, NOT_STARTED_STATUSES, HOST_HEARTBEAT_STALE_MS;
|
|
548
|
+
var init_hosts = __esm({
|
|
549
|
+
"../swarm-core/src/hosts.ts"() {
|
|
550
|
+
"use strict";
|
|
551
|
+
ANY_HOST_TOKENS = /* @__PURE__ */ new Set(["any", "none", "null", "free", "-", "\u043B\u044E\u0431\u0430\u044F", "\u043B\u044E\u0431\u043E\u0439"]);
|
|
552
|
+
UNKNOWN_HOST_KEY = "__no-host__";
|
|
553
|
+
UNKNOWN_HOST_LABEL = "\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430";
|
|
554
|
+
QUEUE_LABEL = "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438";
|
|
555
|
+
NOT_STARTED_STATUSES = /* @__PURE__ */ new Set(["queued"]);
|
|
556
|
+
HOST_HEARTBEAT_STALE_MS = 45e3;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// ../swarm-core/src/relief.ts
|
|
561
|
+
var init_relief = __esm({
|
|
562
|
+
"../swarm-core/src/relief.ts"() {
|
|
563
|
+
"use strict";
|
|
564
|
+
init_time();
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// ../swarm-core/src/index.ts
|
|
569
|
+
var init_src = __esm({
|
|
570
|
+
"../swarm-core/src/index.ts"() {
|
|
571
|
+
"use strict";
|
|
572
|
+
init_types();
|
|
573
|
+
init_timeline();
|
|
574
|
+
init_reviewPhase();
|
|
575
|
+
init_status();
|
|
576
|
+
init_time();
|
|
577
|
+
init_windows();
|
|
578
|
+
init_slug();
|
|
579
|
+
init_text();
|
|
580
|
+
init_comment();
|
|
581
|
+
init_names();
|
|
582
|
+
init_baseBranch();
|
|
583
|
+
init_hosts();
|
|
584
|
+
init_relief();
|
|
585
|
+
}
|
|
586
|
+
});
|
|
491
587
|
|
|
492
588
|
// src/config.ts
|
|
493
|
-
|
|
494
|
-
|
|
589
|
+
import { homedir } from "os";
|
|
590
|
+
import { basename, join, resolve } from "path";
|
|
591
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
|
|
495
592
|
function normalizeRepo(repo) {
|
|
496
593
|
const r = (repo ?? "").trim();
|
|
497
594
|
if (!r) return "";
|
|
@@ -501,15 +598,6 @@ function normalizeRepo(repo) {
|
|
|
501
598
|
return resolve(r);
|
|
502
599
|
}
|
|
503
600
|
}
|
|
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
601
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
514
602
|
return join(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
515
603
|
}
|
|
@@ -519,48 +607,12 @@ function resolveEffectiveWorktreesDir(deps) {
|
|
|
519
607
|
if (deps.liveDronesUnder(deps.legacyDir)) return deps.legacyDir;
|
|
520
608
|
return deps.targetDir;
|
|
521
609
|
}
|
|
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
610
|
function envNum(name, def) {
|
|
526
611
|
const raw = process.env[name];
|
|
527
612
|
if (raw === void 0 || raw === "") return def;
|
|
528
613
|
const n = Number(raw);
|
|
529
614
|
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
530
615
|
}
|
|
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_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
561
|
-
var TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
562
|
-
var SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
563
|
-
var TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
564
616
|
function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
565
617
|
const base3 = api.replace(/\/$/, "");
|
|
566
618
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -573,32 +625,86 @@ function tokenFromFile() {
|
|
|
573
625
|
return "";
|
|
574
626
|
}
|
|
575
627
|
}
|
|
576
|
-
var
|
|
577
|
-
var
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
628
|
+
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;
|
|
629
|
+
var init_config = __esm({
|
|
630
|
+
"src/config.ts"() {
|
|
631
|
+
"use strict";
|
|
632
|
+
init_src();
|
|
633
|
+
SWARM_HOME = process.env.SWARM_HOME || join(homedir(), ".taskless-swarm");
|
|
634
|
+
mkdirSync(SWARM_HOME, { recursive: true });
|
|
635
|
+
DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
|
|
636
|
+
DRONE_HOME = process.env.SWARM_DRONE_HOME || join(SWARM_HOME, "drone-home");
|
|
637
|
+
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
638
|
+
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
639
|
+
WORKTREES_NOINDEX_DIR = join(SWARM_HOME, "worktrees.noindex");
|
|
640
|
+
WORKTREES_LEGACY_DIR = join(SWARM_HOME, "worktrees");
|
|
641
|
+
WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
642
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
|
|
643
|
+
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join(SWARM_HOME, "overmind.log");
|
|
644
|
+
DIST_BUILD = true;
|
|
645
|
+
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
646
|
+
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
|
|
647
|
+
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
648
|
+
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
649
|
+
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
650
|
+
SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
651
|
+
SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
652
|
+
SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
|
|
653
|
+
SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
|
|
654
|
+
SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
|
|
655
|
+
SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
|
|
656
|
+
SWARM_RELIEF_CPU_BUSY = envNum("SWARM_RELIEF_CPU_BUSY", 0.95);
|
|
657
|
+
SWARM_RELIEF_COST_WINDOW_MS = envNum("SWARM_RELIEF_COST_WINDOW_MS", 60 * 6e4);
|
|
658
|
+
SWARM_RELIEF_ALERT_PAUSES = Math.trunc(envNum("SWARM_RELIEF_ALERT_PAUSES", 6));
|
|
659
|
+
SWARM_RELIEF_ALERT_LOST_MS = envNum("SWARM_RELIEF_ALERT_LOST_MS", 10 * 6e4);
|
|
660
|
+
SWARM_RELIEF_ALERT_MIN_STEPS = envNum("SWARM_RELIEF_ALERT_MIN_STEPS", 1);
|
|
661
|
+
SWARM_RELIEF_ALERT_SUSTAIN_MS = envNum("SWARM_RELIEF_ALERT_SUSTAIN_MS", 10 * 6e4);
|
|
662
|
+
SWARM_LOW_DISK_GB = envNum("SWARM_LOW_DISK_GB", 10);
|
|
663
|
+
SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
664
|
+
SWARM_DRONE_BRIDGE_MS = envNum("SWARM_DRONE_BRIDGE_MS", 5e3);
|
|
665
|
+
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
666
|
+
SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
|
|
667
|
+
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
668
|
+
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
669
|
+
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
670
|
+
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
671
|
+
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join(SWARM_HOME, "drone-channel.key");
|
|
672
|
+
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
673
|
+
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
674
|
+
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
675
|
+
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
676
|
+
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
677
|
+
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
678
|
+
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
679
|
+
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
680
|
+
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
681
|
+
TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
|
|
682
|
+
TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
|
|
683
|
+
TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
|
|
684
|
+
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
685
|
+
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
686
|
+
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
687
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
|
|
688
|
+
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
689
|
+
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
690
|
+
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
691
|
+
WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
692
|
+
SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
|
|
693
|
+
SWARM_ANOMALY_MAX_REPEATS = Math.trunc(envNum("SWARM_ANOMALY_MAX_REPEATS", 3));
|
|
694
|
+
SWARM_ANOMALY_RUN_DOWN_WINDOW_MS = envNum("SWARM_ANOMALY_RUN_DOWN_WINDOW_MS", 24 * 60 * 6e4);
|
|
695
|
+
SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
|
|
696
|
+
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
|
|
697
|
+
SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
|
|
698
|
+
SWARM_ANOMALY_PR_STUCK_MERGE_MS = envNum("SWARM_ANOMALY_PR_STUCK_MERGE_MS", 30 * 6e4);
|
|
699
|
+
SWARM_ANOMALY_PR_NO_REVIEWER_MS = envNum("SWARM_ANOMALY_PR_NO_REVIEWER_MS", 20 * 6e4);
|
|
700
|
+
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
701
|
+
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
702
|
+
SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
|
|
703
|
+
SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
599
706
|
|
|
600
707
|
// src/names.ts
|
|
601
|
-
var NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
|
|
602
708
|
function pickName(rows) {
|
|
603
709
|
const live = /* @__PURE__ */ new Set();
|
|
604
710
|
const lastUse = /* @__PURE__ */ new Map();
|
|
@@ -618,21 +724,16 @@ function pickName(rows) {
|
|
|
618
724
|
}
|
|
619
725
|
}
|
|
620
726
|
}
|
|
727
|
+
var NAME_LIVE;
|
|
728
|
+
var init_names2 = __esm({
|
|
729
|
+
"src/names.ts"() {
|
|
730
|
+
"use strict";
|
|
731
|
+
init_src();
|
|
732
|
+
NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
621
735
|
|
|
622
736
|
// src/storm.ts
|
|
623
|
-
var numEnv = (name, def) => {
|
|
624
|
-
const v = Number(process.env[name]);
|
|
625
|
-
return Number.isFinite(v) && v > 0 ? v : def;
|
|
626
|
-
};
|
|
627
|
-
var DEFAULT_STORM_CONFIG = {
|
|
628
|
-
deathThreshold: numEnv("SWARM_STORM_DEATHS", 2),
|
|
629
|
-
windowMs: numEnv("SWARM_STORM_WINDOW_MS", 5 * 60 * 1e3),
|
|
630
|
-
spawnFailThreshold: numEnv("SWARM_STORM_SPAWN_FAILS", 2),
|
|
631
|
-
probeIntervalMs: numEnv("SWARM_STORM_PROBE_INTERVAL_MS", 60 * 1e3),
|
|
632
|
-
probeSuccessesToClear: numEnv("SWARM_STORM_PROBE_CLEAR", 2),
|
|
633
|
-
probeTimeoutMs: numEnv("SWARM_STORM_PROBE_TIMEOUT_MS", 20 * 1e3),
|
|
634
|
-
restartCooldownMs: numEnv("SWARM_STORM_RESTART_COOLDOWN_MS", 3 * 60 * 1e3)
|
|
635
|
-
};
|
|
636
737
|
function encodeProbe(rec) {
|
|
637
738
|
return JSON.stringify(rec);
|
|
638
739
|
}
|
|
@@ -656,76 +757,35 @@ function lastProbeReason(recs) {
|
|
|
656
757
|
}
|
|
657
758
|
return null;
|
|
658
759
|
}
|
|
760
|
+
var numEnv, DEFAULT_STORM_CONFIG;
|
|
761
|
+
var init_storm = __esm({
|
|
762
|
+
"src/storm.ts"() {
|
|
763
|
+
"use strict";
|
|
764
|
+
init_src();
|
|
765
|
+
numEnv = (name, def) => {
|
|
766
|
+
const v = Number(process.env[name]);
|
|
767
|
+
return Number.isFinite(v) && v > 0 ? v : def;
|
|
768
|
+
};
|
|
769
|
+
DEFAULT_STORM_CONFIG = {
|
|
770
|
+
deathThreshold: numEnv("SWARM_STORM_DEATHS", 2),
|
|
771
|
+
windowMs: numEnv("SWARM_STORM_WINDOW_MS", 5 * 60 * 1e3),
|
|
772
|
+
spawnFailThreshold: numEnv("SWARM_STORM_SPAWN_FAILS", 2),
|
|
773
|
+
probeIntervalMs: numEnv("SWARM_STORM_PROBE_INTERVAL_MS", 60 * 1e3),
|
|
774
|
+
probeSuccessesToClear: numEnv("SWARM_STORM_PROBE_CLEAR", 2),
|
|
775
|
+
probeTimeoutMs: numEnv("SWARM_STORM_PROBE_TIMEOUT_MS", 20 * 1e3),
|
|
776
|
+
restartCooldownMs: numEnv("SWARM_STORM_RESTART_COOLDOWN_MS", 3 * 60 * 1e3)
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
});
|
|
659
780
|
|
|
660
781
|
// src/db.ts
|
|
661
|
-
var
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
kind TEXT NOT NULL DEFAULT 'drone',
|
|
669
|
-
hero_run_id INTEGER,
|
|
670
|
-
repo TEXT,
|
|
671
|
-
worktree_path TEXT,
|
|
672
|
-
branch TEXT,
|
|
673
|
-
status TEXT NOT NULL DEFAULT 'queued',
|
|
674
|
-
pid INTEGER,
|
|
675
|
-
area TEXT,
|
|
676
|
-
depends_on TEXT,
|
|
677
|
-
model TEXT,
|
|
678
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
679
|
-
pr_url TEXT,
|
|
680
|
-
superseded_by INTEGER,
|
|
681
|
-
decision TEXT,
|
|
682
|
-
stop_reason TEXT,
|
|
683
|
-
respawns INTEGER NOT NULL DEFAULT 0,
|
|
684
|
-
resolves_run_id INTEGER,
|
|
685
|
-
reviews_run_id INTEGER,
|
|
686
|
-
review_verdict TEXT,
|
|
687
|
-
review_cycle INTEGER NOT NULL DEFAULT 0,
|
|
688
|
-
task_title TEXT,
|
|
689
|
-
task_desc TEXT,
|
|
690
|
-
task_url TEXT,
|
|
691
|
-
priority INTEGER NOT NULL DEFAULT 0,
|
|
692
|
-
base_branch TEXT,
|
|
693
|
-
workspace_id TEXT,
|
|
694
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
695
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
696
|
-
);
|
|
697
|
-
CREATE TABLE IF NOT EXISTS questions (
|
|
698
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
699
|
-
run_id INTEGER NOT NULL REFERENCES runs(id),
|
|
700
|
-
text TEXT NOT NULL,
|
|
701
|
-
options TEXT,
|
|
702
|
-
status TEXT NOT NULL DEFAULT 'open',
|
|
703
|
-
answer TEXT,
|
|
704
|
-
answered_by TEXT,
|
|
705
|
-
escalated INTEGER NOT NULL DEFAULT 0,
|
|
706
|
-
asked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
707
|
-
answered_at TEXT
|
|
708
|
-
);
|
|
709
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
710
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
711
|
-
run_id INTEGER REFERENCES runs(id),
|
|
712
|
-
kind TEXT NOT NULL,
|
|
713
|
-
text TEXT,
|
|
714
|
-
actor TEXT,
|
|
715
|
-
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
716
|
-
);
|
|
717
|
-
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
718
|
-
CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
|
|
719
|
-
CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
|
|
720
|
-
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
721
|
-
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
|
|
722
|
-
-- \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)
|
|
723
|
-
CREATE TABLE IF NOT EXISTS imports (
|
|
724
|
-
source TEXT PRIMARY KEY,
|
|
725
|
-
run_count INTEGER NOT NULL DEFAULT 0,
|
|
726
|
-
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
727
|
-
);
|
|
728
|
-
`;
|
|
782
|
+
var db_exports = {};
|
|
783
|
+
__export(db_exports, {
|
|
784
|
+
SwarmDb: () => SwarmDb
|
|
785
|
+
});
|
|
786
|
+
import { createRequire as createRequire2 } from "module";
|
|
787
|
+
import { existsSync as existsSync8 } from "fs";
|
|
788
|
+
import { resolve as resolve2 } from "path";
|
|
729
789
|
function rowToQuestion(row) {
|
|
730
790
|
return {
|
|
731
791
|
id: row.id,
|
|
@@ -788,7 +848,7 @@ function isForeignKeyError(err) {
|
|
|
788
848
|
return err instanceof Error && err.code === "SQLITE_CONSTRAINT_FOREIGNKEY";
|
|
789
849
|
}
|
|
790
850
|
function loadBetterSqlite3() {
|
|
791
|
-
const require2 =
|
|
851
|
+
const require2 = createRequire2(import.meta.url);
|
|
792
852
|
try {
|
|
793
853
|
return require2("better-sqlite3");
|
|
794
854
|
} catch (err) {
|
|
@@ -797,668 +857,753 @@ function loadBetterSqlite3() {
|
|
|
797
857
|
);
|
|
798
858
|
}
|
|
799
859
|
}
|
|
800
|
-
var
|
|
801
|
-
|
|
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
|
-
|
|
860
|
+
var TERMINAL_SQL, RESOLVER_TERMINAL_SQL, SCHEMA, SwarmDb;
|
|
861
|
+
var init_db = __esm({
|
|
862
|
+
"src/db.ts"() {
|
|
863
|
+
"use strict";
|
|
864
|
+
init_config();
|
|
865
|
+
init_names2();
|
|
866
|
+
init_storm();
|
|
867
|
+
init_src();
|
|
868
|
+
TERMINAL_SQL = TERMINAL_STATUSES.map((s) => `'${s}'`).join(",");
|
|
869
|
+
RESOLVER_TERMINAL_SQL = [...TERMINAL_STATUSES, "stalled"].map((s) => `'${s}'`).join(",");
|
|
870
|
+
SCHEMA = `
|
|
871
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
872
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
873
|
+
task_slug TEXT NOT NULL,
|
|
874
|
+
name TEXT,
|
|
875
|
+
kind TEXT NOT NULL DEFAULT 'drone',
|
|
876
|
+
hero_run_id INTEGER,
|
|
877
|
+
repo TEXT,
|
|
878
|
+
worktree_path TEXT,
|
|
879
|
+
branch TEXT,
|
|
880
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
881
|
+
pid INTEGER,
|
|
882
|
+
area TEXT,
|
|
883
|
+
depends_on TEXT,
|
|
884
|
+
model TEXT,
|
|
885
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
886
|
+
pr_url TEXT,
|
|
887
|
+
superseded_by INTEGER,
|
|
888
|
+
decision TEXT,
|
|
889
|
+
stop_reason TEXT,
|
|
890
|
+
respawns INTEGER NOT NULL DEFAULT 0,
|
|
891
|
+
resolves_run_id INTEGER,
|
|
892
|
+
reviews_run_id INTEGER,
|
|
893
|
+
review_verdict TEXT,
|
|
894
|
+
review_cycle INTEGER NOT NULL DEFAULT 0,
|
|
895
|
+
task_title TEXT,
|
|
896
|
+
task_desc TEXT,
|
|
897
|
+
task_url TEXT,
|
|
898
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
899
|
+
base_branch TEXT,
|
|
900
|
+
workspace_id TEXT,
|
|
901
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
902
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
903
|
+
);
|
|
904
|
+
CREATE TABLE IF NOT EXISTS questions (
|
|
905
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
906
|
+
run_id INTEGER NOT NULL REFERENCES runs(id),
|
|
907
|
+
text TEXT NOT NULL,
|
|
908
|
+
options TEXT,
|
|
909
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
910
|
+
answer TEXT,
|
|
911
|
+
answered_by TEXT,
|
|
912
|
+
escalated INTEGER NOT NULL DEFAULT 0,
|
|
913
|
+
asked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
914
|
+
answered_at TEXT
|
|
915
|
+
);
|
|
916
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
917
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
918
|
+
run_id INTEGER REFERENCES runs(id),
|
|
919
|
+
kind TEXT NOT NULL,
|
|
920
|
+
text TEXT,
|
|
921
|
+
actor TEXT,
|
|
922
|
+
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
923
|
+
);
|
|
924
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
925
|
+
CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
|
|
926
|
+
CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
|
|
927
|
+
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
928
|
+
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
|
|
929
|
+
-- \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)
|
|
930
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
931
|
+
source TEXT PRIMARY KEY,
|
|
932
|
+
run_count INTEGER NOT NULL DEFAULT 0,
|
|
933
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
934
|
+
);
|
|
935
|
+
`;
|
|
936
|
+
SwarmDb = class {
|
|
937
|
+
db;
|
|
938
|
+
defaultRepo;
|
|
939
|
+
databaseCtor;
|
|
940
|
+
// для importDb (второй Database, source)
|
|
941
|
+
constructor(path = "swarm.db", defaultRepo = "") {
|
|
942
|
+
this.defaultRepo = normalizeRepo(defaultRepo);
|
|
943
|
+
this.databaseCtor = loadBetterSqlite3();
|
|
944
|
+
this.db = new this.databaseCtor(path);
|
|
945
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
946
|
+
this.db.exec(SCHEMA);
|
|
947
|
+
for (const alter of [
|
|
948
|
+
"ALTER TABLE questions ADD COLUMN options TEXT",
|
|
949
|
+
"ALTER TABLE questions ADD COLUMN escalated INTEGER NOT NULL DEFAULT 0",
|
|
950
|
+
"ALTER TABLE questions ADD COLUMN answered_by TEXT",
|
|
951
|
+
"ALTER TABLE runs ADD COLUMN superseded_by INTEGER",
|
|
952
|
+
"ALTER TABLE runs ADD COLUMN decision TEXT",
|
|
953
|
+
"ALTER TABLE runs ADD COLUMN stop_reason TEXT",
|
|
954
|
+
"ALTER TABLE runs ADD COLUMN repo TEXT",
|
|
955
|
+
"ALTER TABLE runs ADD COLUMN name TEXT",
|
|
956
|
+
"ALTER TABLE runs ADD COLUMN respawns INTEGER NOT NULL DEFAULT 0",
|
|
957
|
+
"ALTER TABLE runs ADD COLUMN resolves_run_id INTEGER",
|
|
958
|
+
"ALTER TABLE runs ADD COLUMN depends_on TEXT",
|
|
959
|
+
"ALTER TABLE runs ADD COLUMN task_title TEXT",
|
|
960
|
+
"ALTER TABLE runs ADD COLUMN task_desc TEXT",
|
|
961
|
+
"ALTER TABLE runs ADD COLUMN task_url TEXT",
|
|
962
|
+
// Иерархия роя (TASWM-78): роль прогона (drone|hero) и ссылка на прогон-героя.
|
|
963
|
+
"ALTER TABLE runs ADD COLUMN kind TEXT NOT NULL DEFAULT 'drone'",
|
|
964
|
+
"ALTER TABLE runs ADD COLUMN hero_run_id INTEGER",
|
|
965
|
+
"ALTER TABLE events ADD COLUMN actor TEXT",
|
|
966
|
+
// Авто-ревью (TASWM-148): прогон-ревьюер (reviews_run_id → review-прогон) и
|
|
967
|
+
// вердикт (approve|changes), который ревьюер пишет на проверяемый прогон.
|
|
968
|
+
"ALTER TABLE runs ADD COLUMN reviews_run_id INTEGER",
|
|
969
|
+
"ALTER TABLE runs ADD COLUMN review_verdict TEXT",
|
|
970
|
+
// Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
|
|
971
|
+
"ALTER TABLE runs ADD COLUMN review_cycle INTEGER NOT NULL DEFAULT 0",
|
|
972
|
+
// Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
|
|
973
|
+
"ALTER TABLE runs ADD COLUMN review_phase TEXT",
|
|
974
|
+
// Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
|
|
975
|
+
"ALTER TABLE runs ADD COLUMN priority INTEGER NOT NULL DEFAULT 0",
|
|
976
|
+
// Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
|
|
977
|
+
"ALTER TABLE runs ADD COLUMN base_branch TEXT",
|
|
978
|
+
// Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
|
|
979
|
+
"ALTER TABLE runs ADD COLUMN workspace_id TEXT"
|
|
980
|
+
]) {
|
|
981
|
+
try {
|
|
982
|
+
this.db.exec(alter);
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_repo ON runs(repo)");
|
|
987
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_hero ON runs(hero_run_id)");
|
|
988
|
+
if (this.defaultRepo) {
|
|
989
|
+
this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
|
|
990
|
+
}
|
|
848
991
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_hero ON runs(hero_run_id)");
|
|
852
|
-
if (this.defaultRepo) {
|
|
853
|
-
this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
close() {
|
|
857
|
-
this.db.close();
|
|
858
|
-
}
|
|
859
|
-
// --- runs ---
|
|
860
|
-
createRun(input) {
|
|
861
|
-
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
862
|
-
const res = this.db.prepare(
|
|
863
|
-
"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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
864
|
-
).run(
|
|
865
|
-
input.task_slug,
|
|
866
|
-
name,
|
|
867
|
-
input.kind ?? "drone",
|
|
868
|
-
// роль прогона: дефолт — рабочий дрон
|
|
869
|
-
input.hero_run_id ?? null,
|
|
870
|
-
// отряд героя (задел иерархии, TASWM-78)
|
|
871
|
-
normalizeRepo(input.repo || this.defaultRepo),
|
|
872
|
-
// канон при записи → один area-lock на физрепо
|
|
873
|
-
input.area ?? null,
|
|
874
|
-
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
875
|
-
input.model ?? null,
|
|
876
|
-
input.decision ? JSON.stringify(input.decision) : null,
|
|
877
|
-
input.resolves_run_id ?? null,
|
|
878
|
-
input.reviews_run_id ?? null,
|
|
879
|
-
input.review_cycle ?? 0,
|
|
880
|
-
// цикл ревью для ревьюера (авто-ревью v2, TASWM-162); обычный прогон — 0
|
|
881
|
-
input.priority ?? 0,
|
|
882
|
-
// явный приоритет в очереди (TASWM-269); обычный прогон — 0
|
|
883
|
-
input.base_branch ?? null,
|
|
884
|
-
// базовая ветка PR (TASWM-285); null → проектная/дефолтная
|
|
885
|
-
input.workspace_id ?? null
|
|
886
|
-
// воркспейс задачи прогона (TASWM-288); null → глобальный пин
|
|
887
|
-
);
|
|
888
|
-
const id = Number(res.lastInsertRowid);
|
|
889
|
-
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}`;
|
|
890
|
-
this.addEvent(id, "spawned", spawnNote);
|
|
891
|
-
return this.getRun(id);
|
|
892
|
-
}
|
|
893
|
-
/**
|
|
894
|
-
* Завести стаб-строку прогона #id, если её ещё нет (TASWM-263, «ревьюеры не могут сдать
|
|
895
|
-
* работу»). Корень: демон в client-режиме (SWARM_BACKEND=client, опубликованный npm-пакет)
|
|
896
|
-
* ведёт runs в in-memory зеркале SwarmClient + проде (swarm_run_upsert), а НЕ в этой sqlite —
|
|
897
|
-
* но дрон-MCP-сервер (droneServer.ts:runDroneServer) ВСЕГДА открывает SwarmDb на локальный
|
|
898
|
-
* DB_PATH, независимо от бэкенда демона. Спавненный из прода прогон (claim/hydrate) в этой
|
|
899
|
-
* БД строки не получает → первый же write-путь (report_step/ask/done) падает на FK
|
|
900
|
-
* (events.run_id/questions.run_id REFERENCES runs(id)), а ревьюер не может ни отчитаться,
|
|
901
|
-
* ни донести вердикт. INSERT OR IGNORE — идемпотентно (конкурентный вызов/уже заведённая
|
|
902
|
-
* строка не трогаются, id explicit-инсертом двигает автоинкремент дальше, коллизий с
|
|
903
|
-
* будущими createRun не будет). status='running' — прогон уже фактически работает (иначе
|
|
904
|
-
* не звал бы write-путь). kind/reviews_run_id/hero_run_id — ЛУЧШЕЕ известное из данных
|
|
905
|
-
* спавна (runDroneServer читает их из SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID,
|
|
906
|
-
* см. launcher.ts): дефолт "drone" без них исказил бы done() ревьюера — тот принял бы свой
|
|
907
|
-
* вердикт за обычную сдачу PR (неверная ветка кода в droneServer.done). name — та же
|
|
908
|
-
* история (TASWM-265): без явного имени колонка оставалась NULL, а rowToRun подставлял
|
|
909
|
-
* `#id` — droneServer.done() подписывал отчёт номером прогона вместо имени дрона (номер
|
|
910
|
-
* локальный и читателю задачи ничего не говорит). Честный fallback — заглушка
|
|
911
|
-
* "(имя неизвестно)", а не номер, выданный за имя.
|
|
912
|
-
*/
|
|
913
|
-
ensureRun(input) {
|
|
914
|
-
this.db.prepare(
|
|
915
|
-
"INSERT OR IGNORE INTO runs (id, task_slug, kind, status, name, reviews_run_id, hero_run_id) VALUES (?, ?, ?, 'running', ?, ?, ?)"
|
|
916
|
-
).run(
|
|
917
|
-
input.id,
|
|
918
|
-
input.task_slug,
|
|
919
|
-
input.kind ?? "drone",
|
|
920
|
-
input.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)",
|
|
921
|
-
input.reviews_run_id ?? null,
|
|
922
|
-
input.hero_run_id ?? null
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
getRun(id) {
|
|
926
|
-
const row = this.db.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
927
|
-
return row ? rowToRun(row) : null;
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Стабильный ключ прогона (TASWM-295). У sqlite он тривиален: id — настоящий PK, живущий
|
|
931
|
-
* в файле, поэтому ключ это и есть номер с префиксом. Префикс не декоративный: он не даёт
|
|
932
|
-
* перепутать пространства ключей, если токен канала, выписанный демоном на sqlite-бэкенде,
|
|
933
|
-
* попадёт в демона на client-бэкенде (там ключи вида `p:<cuid>`) — резолв просто не найдёт
|
|
934
|
-
* прогон, вместо того чтобы наткнуться на чужой с тем же номером.
|
|
935
|
-
*/
|
|
936
|
-
runKeyOf(id) {
|
|
937
|
-
return this.getRun(id) ? `local:${id}` : null;
|
|
938
|
-
}
|
|
939
|
-
runByKey(key) {
|
|
940
|
-
const m = /^local:(\d+)$/.exec(key);
|
|
941
|
-
return m ? this.getRun(Number(m[1])) : null;
|
|
942
|
-
}
|
|
943
|
-
listRuns(status) {
|
|
944
|
-
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();
|
|
945
|
-
return rows.map(rowToRun);
|
|
946
|
-
}
|
|
947
|
-
/** Прогоны отряда Героя-лейтенанта #heroId (его дроны: hero_run_id = heroId), новые сверху (TASWM-81). */
|
|
948
|
-
squadRuns(heroId) {
|
|
949
|
-
const rows = this.db.prepare("SELECT * FROM runs WHERE hero_run_id = ? ORDER BY id DESC").all(heroId);
|
|
950
|
-
return rows.map(rowToRun);
|
|
951
|
-
}
|
|
952
|
-
updateRun(id, patch) {
|
|
953
|
-
const fields = Object.keys(patch);
|
|
954
|
-
if (fields.length === 0) return this.getRun(id);
|
|
955
|
-
const set = fields.map((f) => `${f} = ?`).join(", ");
|
|
956
|
-
const vals = fields.map((f) => {
|
|
957
|
-
const v = patch[f];
|
|
958
|
-
if (f === "depends_on") {
|
|
959
|
-
const arr = v;
|
|
960
|
-
return arr && arr.length ? JSON.stringify(arr) : null;
|
|
992
|
+
close() {
|
|
993
|
+
this.db.close();
|
|
961
994
|
}
|
|
962
|
-
|
|
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
|
-
return row.n;
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
|
|
1030
|
-
* ВСЕГО (любой статус, включая failed/stalled/stopped — специально в отличие от
|
|
1031
|
-
* reviewerCountFor выше) поднималось на review-прогон #reviewRunId В ТЕКУЩЕМ ЦИКЛЕ (скоуп
|
|
1032
|
-
* по циклу — авто-ревью v2, TASWM-162). Зеркалит resolverCountFor — плоский потолок
|
|
1033
|
-
* попыток на цикл. Именно это ловит карусель: reviewerCountFor исключает failed/stalled
|
|
1034
|
-
* (чтобы ОДИН транзиентный сбой не блокировал ревью), но тем самым допускает бесконечный
|
|
1035
|
-
* ретрай, если ревьюер стабильно умирает без done(). Скоуп по циклу — чтобы кап считался
|
|
1036
|
-
* заново на каждой фикс-итерации, а не суммировал попытки прошлых (успешно завершённых)
|
|
1037
|
-
* циклов. superseded — ЕДИНСТВЕННОЕ исключение из капа (TASWM-253): это статус, которым
|
|
1038
|
-
* POST /swarm/review/retry помечает попытки при операторском «попробовать ревью заново» —
|
|
1039
|
-
* без исключения сброс не мог бы ничего сбросить (COUNT(*) слеп к статусу). failed/
|
|
1040
|
-
* stalled/stopped по-прежнему считаются — иначе кап потерял бы смысл на органических
|
|
1041
|
-
* смертях (ровно то, для чего его вводили в TASWM-204).
|
|
1042
|
-
*/
|
|
1043
|
-
reviewerAttemptCountFor(reviewRunId) {
|
|
1044
|
-
const row = this.db.prepare(
|
|
1045
|
-
"SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status != 'superseded' AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0)"
|
|
1046
|
-
).get(reviewRunId, reviewRunId);
|
|
1047
|
-
return row.n;
|
|
1048
|
-
}
|
|
1049
|
-
/** Последняя (любой статус) попытка ревьюера ТЕКУЩЕГО ЦИКЛА на review-прогон #reviewRunId
|
|
1050
|
-
* (TASWM-196; скоуп по циклу — TASWM-162) — см. store.ts:lastReviewerAttemptFor. */
|
|
1051
|
-
lastReviewerAttemptFor(reviewRunId) {
|
|
1052
|
-
const row = this.db.prepare(
|
|
1053
|
-
"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"
|
|
1054
|
-
).get(reviewRunId, reviewRunId);
|
|
1055
|
-
return row ? rowToRun(row) : null;
|
|
1056
|
-
}
|
|
1057
|
-
/**
|
|
1058
|
-
* Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
|
|
1059
|
-
* closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
|
|
1060
|
-
* не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
|
|
1061
|
-
*/
|
|
1062
|
-
activeReviewersFor(reviewRunId) {
|
|
1063
|
-
const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
|
|
1064
|
-
return rows.map(rowToRun);
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Последний живой (не терминальный) прогон по slug — для swarm cancel. Служебные
|
|
1068
|
-
* сабраны (reviews_run_id/resolves_run_id — ревьюер/резолвер, TASWM-273) исключены:
|
|
1069
|
-
* они заводятся с ТЕМ ЖЕ task_slug, что и родительский прогон, и всегда бо́льшим id
|
|
1070
|
-
* (спавнятся позже) — без исключения `ORDER BY id DESC LIMIT 1` подсовывал бы
|
|
1071
|
-
* cancelRun паузнутого ревьюера (paused НЕ терминален) вместо реального прогона,
|
|
1072
|
-
* которого целился остановить оператор/`swarm cancel <slug>`: безусловный removeWorktree
|
|
1073
|
-
* в cancelRun сносил бы worktree ревьюера, ждущего CI, и переводил его в stopped мимо
|
|
1074
|
-
* обычной цепочки паузы/побудки — найдено при разборе TASWM-273 (PR #564 завис в stalled
|
|
1075
|
-
* без вердикта: worktree ревьюера пропал так, что статус до этого не менялся). Сабран
|
|
1076
|
-
* отменяют через свой id, не через slug — swarm cancel всегда целится в родительский прогон.
|
|
1077
|
-
*/
|
|
1078
|
-
activeRunForSlug(slug) {
|
|
1079
|
-
const row = this.db.prepare(
|
|
1080
|
-
`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`
|
|
1081
|
-
).get(slug);
|
|
1082
|
-
return row ? rowToRun(row) : null;
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Зоны, занятые сейчас работающими дронами — для конфликт-осознанной раздачи.
|
|
1086
|
-
* С repo — только в этом репозитории (одна area в разных репо не конфликтует).
|
|
1087
|
-
* Без repo — по всему рою (обратная совместимость).
|
|
1088
|
-
*/
|
|
1089
|
-
runningAreas(repo) {
|
|
1090
|
-
const base3 = "SELECT DISTINCT area FROM runs WHERE status IN ('running','blocked') AND area IS NOT NULL";
|
|
1091
|
-
const rows = repo === void 0 ? this.db.prepare(base3).all() : this.db.prepare(`${base3} AND repo = ?`).all(repo);
|
|
1092
|
-
return rows.map((r) => r.area);
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
|
|
1096
|
-
* статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
|
|
1097
|
-
* (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
|
|
1098
|
-
* а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
|
|
1099
|
-
* Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
|
|
1100
|
-
* но очередь не виснет (гейт — пропуск в цикле, не блокировка).
|
|
1101
|
-
*/
|
|
1102
|
-
unmetDeps(depends_on) {
|
|
1103
|
-
if (!depends_on || depends_on.length === 0) return [];
|
|
1104
|
-
const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
|
|
1105
|
-
const done = new Set(rows.map((r) => r.task_slug));
|
|
1106
|
-
return depends_on.filter((slug) => !done.has(slug));
|
|
1107
|
-
}
|
|
1108
|
-
/** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
|
|
1109
|
-
distinctRepos() {
|
|
1110
|
-
const rows = this.db.prepare("SELECT DISTINCT repo FROM runs WHERE repo IS NOT NULL AND repo <> ''").all();
|
|
1111
|
-
return rows.map((r) => r.repo);
|
|
1112
|
-
}
|
|
1113
|
-
// --- импорт истории из другой БД (объединение роёв) ---
|
|
1114
|
-
/**
|
|
1115
|
-
* Идемпотентный перенос runs/events/questions из другой swarm-БД в эту.
|
|
1116
|
-
* id прогонов ремапятся (старый→новый); run_id в events/questions и superseded_by
|
|
1117
|
-
* в runs правятся по карте; всем импортируемым прогонам проставляется repo (норм.).
|
|
1118
|
-
* pid НЕ переносим — чужой pid в объединённой БД бессмыслен и мог бы совпасть с
|
|
1119
|
-
* живым процессом (ложный liveness); импортируемые прогоны исторические.
|
|
1120
|
-
* Живые статусы нормализуем (running/blocked→stalled, queued→stopped) и открытые
|
|
1121
|
-
* вопросы не тащим — иначе импорт даёт вечных зомби: без pid death-detect их
|
|
1122
|
-
* пропускает, они навсегда держат (repo,area)-лок и «в работе» в метриках, а их
|
|
1123
|
-
* открытые вопросы «воскрешают» архивный прогон через answer.
|
|
1124
|
-
* Повторный вызов на том же source — no-op (маркер в таблице imports). Source
|
|
1125
|
-
* открываем на ЗАПИСЬ (importDb только ЧИТАЕТ из него): все БД роя — WAL, а bun:sqlite
|
|
1126
|
-
* с readonly НЕ открывает WAL-source без -shm (реальный перенос обычно = копия одного
|
|
1127
|
-
* файла swarm.db без sidecar'ов → SQLITE_CANTOPEN). Несуществующий путь ловим existsSync
|
|
1128
|
-
* ДО открытия — честная ошибка, а не пустой файл (rw создал бы его). Всё в одной
|
|
1129
|
-
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
1130
|
-
*/
|
|
1131
|
-
importDb(sourcePath, repo) {
|
|
1132
|
-
const abs = resolve2(sourcePath);
|
|
1133
|
-
const repoN = normalizeRepo(repo);
|
|
1134
|
-
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
1135
|
-
if (already) return { imported: false, runs: already.run_count };
|
|
1136
|
-
if (!existsSync(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
1137
|
-
const src = new this.databaseCtor(abs);
|
|
1138
|
-
try {
|
|
1139
|
-
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
1140
|
-
const events = src.prepare("SELECT * FROM events ORDER BY id").all();
|
|
1141
|
-
const questions = src.prepare("SELECT * FROM questions ORDER BY id").all();
|
|
1142
|
-
const txn = this.db.transaction(() => {
|
|
1143
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
1144
|
-
const insRun = this.db.prepare(
|
|
1145
|
-
"INSERT INTO runs (task_slug, repo, worktree_path, branch, status, area, model, cost_usd, pr_url, decision, stop_reason, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
995
|
+
// --- runs ---
|
|
996
|
+
createRun(input) {
|
|
997
|
+
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
998
|
+
const res = this.db.prepare(
|
|
999
|
+
"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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
1000
|
+
).run(
|
|
1001
|
+
input.task_slug,
|
|
1002
|
+
name,
|
|
1003
|
+
input.kind ?? "drone",
|
|
1004
|
+
// роль прогона: дефолт — рабочий дрон
|
|
1005
|
+
input.hero_run_id ?? null,
|
|
1006
|
+
// отряд героя (задел иерархии, TASWM-78)
|
|
1007
|
+
normalizeRepo(input.repo || this.defaultRepo),
|
|
1008
|
+
// канон при записи → один area-lock на физрепо
|
|
1009
|
+
input.area ?? null,
|
|
1010
|
+
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
1011
|
+
input.model ?? null,
|
|
1012
|
+
input.decision ? JSON.stringify(input.decision) : null,
|
|
1013
|
+
input.resolves_run_id ?? null,
|
|
1014
|
+
input.reviews_run_id ?? null,
|
|
1015
|
+
input.review_cycle ?? 0,
|
|
1016
|
+
// цикл ревью для ревьюера (авто-ревью v2, TASWM-162); обычный прогон — 0
|
|
1017
|
+
input.priority ?? 0,
|
|
1018
|
+
// явный приоритет в очереди (TASWM-269); обычный прогон — 0
|
|
1019
|
+
input.base_branch ?? null,
|
|
1020
|
+
// базовая ветка PR (TASWM-285); null → проектная/дефолтная
|
|
1021
|
+
input.workspace_id ?? null
|
|
1022
|
+
// воркспейс задачи прогона (TASWM-288); null → глобальный пин
|
|
1023
|
+
);
|
|
1024
|
+
const id = Number(res.lastInsertRowid);
|
|
1025
|
+
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}`;
|
|
1026
|
+
this.addEvent(id, "spawned", spawnNote);
|
|
1027
|
+
return this.getRun(id);
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Завести стаб-строку прогона #id, если её ещё нет (TASWM-263, «ревьюеры не могут сдать
|
|
1031
|
+
* работу»). Корень: демон в client-режиме (SWARM_BACKEND=client, опубликованный npm-пакет)
|
|
1032
|
+
* ведёт runs в in-memory зеркале SwarmClient + проде (swarm_run_upsert), а НЕ в этой sqlite —
|
|
1033
|
+
* но дрон-MCP-сервер (droneServer.ts:runDroneServer) ВСЕГДА открывает SwarmDb на локальный
|
|
1034
|
+
* DB_PATH, независимо от бэкенда демона. Спавненный из прода прогон (claim/hydrate) в этой
|
|
1035
|
+
* БД строки не получает → первый же write-путь (report_step/ask/done) падает на FK
|
|
1036
|
+
* (events.run_id/questions.run_id REFERENCES runs(id)), а ревьюер не может ни отчитаться,
|
|
1037
|
+
* ни донести вердикт. INSERT OR IGNORE — идемпотентно (конкурентный вызов/уже заведённая
|
|
1038
|
+
* строка не трогаются, id explicit-инсертом двигает автоинкремент дальше, коллизий с
|
|
1039
|
+
* будущими createRun не будет). status='running' — прогон уже фактически работает (иначе
|
|
1040
|
+
* не звал бы write-путь). kind/reviews_run_id/hero_run_id — ЛУЧШЕЕ известное из данных
|
|
1041
|
+
* спавна (runDroneServer читает их из SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID,
|
|
1042
|
+
* см. launcher.ts): дефолт "drone" без них исказил бы done() ревьюера — тот принял бы свой
|
|
1043
|
+
* вердикт за обычную сдачу PR (неверная ветка кода в droneServer.done). name — та же
|
|
1044
|
+
* история (TASWM-265): без явного имени колонка оставалась NULL, а rowToRun подставлял
|
|
1045
|
+
* `#id` — droneServer.done() подписывал отчёт номером прогона вместо имени дрона (номер
|
|
1046
|
+
* локальный и читателю задачи ничего не говорит). Честный fallback — заглушка
|
|
1047
|
+
* "(имя неизвестно)", а не номер, выданный за имя.
|
|
1048
|
+
*/
|
|
1049
|
+
ensureRun(input) {
|
|
1050
|
+
this.db.prepare(
|
|
1051
|
+
"INSERT OR IGNORE INTO runs (id, task_slug, kind, status, name, reviews_run_id, hero_run_id) VALUES (?, ?, ?, 'running', ?, ?, ?)"
|
|
1052
|
+
).run(
|
|
1053
|
+
input.id,
|
|
1054
|
+
input.task_slug,
|
|
1055
|
+
input.kind ?? "drone",
|
|
1056
|
+
input.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)",
|
|
1057
|
+
input.reviews_run_id ?? null,
|
|
1058
|
+
input.hero_run_id ?? null
|
|
1146
1059
|
);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1060
|
+
}
|
|
1061
|
+
getRun(id) {
|
|
1062
|
+
const row = this.db.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
1063
|
+
return row ? rowToRun(row) : null;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Стабильный ключ прогона (TASWM-295). У sqlite он тривиален: id — настоящий PK, живущий
|
|
1067
|
+
* в файле, поэтому ключ это и есть номер с префиксом. Префикс не декоративный: он не даёт
|
|
1068
|
+
* перепутать пространства ключей, если токен канала, выписанный демоном на sqlite-бэкенде,
|
|
1069
|
+
* попадёт в демона на client-бэкенде (там ключи вида `p:<cuid>`) — резолв просто не найдёт
|
|
1070
|
+
* прогон, вместо того чтобы наткнуться на чужой с тем же номером.
|
|
1071
|
+
*/
|
|
1072
|
+
runKeyOf(id) {
|
|
1073
|
+
return this.getRun(id) ? `local:${id}` : null;
|
|
1074
|
+
}
|
|
1075
|
+
runByKey(key) {
|
|
1076
|
+
const m = /^local:(\d+)$/.exec(key);
|
|
1077
|
+
return m ? this.getRun(Number(m[1])) : null;
|
|
1078
|
+
}
|
|
1079
|
+
listRuns(status) {
|
|
1080
|
+
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();
|
|
1081
|
+
return rows.map(rowToRun);
|
|
1082
|
+
}
|
|
1083
|
+
/** Прогоны отряда Героя-лейтенанта #heroId (его дроны: hero_run_id = heroId), новые сверху (TASWM-81). */
|
|
1084
|
+
squadRuns(heroId) {
|
|
1085
|
+
const rows = this.db.prepare("SELECT * FROM runs WHERE hero_run_id = ? ORDER BY id DESC").all(heroId);
|
|
1086
|
+
return rows.map(rowToRun);
|
|
1087
|
+
}
|
|
1088
|
+
updateRun(id, patch) {
|
|
1089
|
+
const fields = Object.keys(patch);
|
|
1090
|
+
if (fields.length === 0) return this.getRun(id);
|
|
1091
|
+
const set = fields.map((f) => `${f} = ?`).join(", ");
|
|
1092
|
+
const vals = fields.map((f) => {
|
|
1093
|
+
const v = patch[f];
|
|
1094
|
+
if (f === "depends_on") {
|
|
1095
|
+
const arr = v;
|
|
1096
|
+
return arr && arr.length ? JSON.stringify(arr) : null;
|
|
1159
1097
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1098
|
+
return v;
|
|
1099
|
+
});
|
|
1100
|
+
this.db.prepare(`UPDATE runs SET ${set}, updated_at = datetime('now') WHERE id = ?`).run(...vals, id);
|
|
1101
|
+
return this.getRun(id);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Атомарно увеличить счётчик авто-оживлений прогона (после смерти дрона рой
|
|
1105
|
+
* сам его respawn'ит/--continue'ит). Возвращает новое значение — по нему
|
|
1106
|
+
* death-detect решает, не исчерпан ли лимит (2 оживления на прогон).
|
|
1107
|
+
*/
|
|
1108
|
+
incRespawn(id) {
|
|
1109
|
+
this.db.prepare("UPDATE runs SET respawns = respawns + 1, updated_at = datetime('now') WHERE id = ?").run(id);
|
|
1110
|
+
const row = this.db.prepare("SELECT respawns FROM runs WHERE id = ?").get(id);
|
|
1111
|
+
return row?.respawns ?? 0;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Остановить прогон оператором: статус "stopped" + причина + событие.
|
|
1115
|
+
* Прогон НЕ удаляется — остаётся в БД и в истории навсегда (в отличие от failed
|
|
1116
|
+
* с выброшенным worktree мы храним причину и запись). Kill/cleanup — снаружи (cancelRun).
|
|
1117
|
+
*/
|
|
1118
|
+
cancel(id, reason) {
|
|
1119
|
+
const run = this.updateRun(id, { status: "stopped", stop_reason: reason });
|
|
1120
|
+
if (run) this.addEvent(id, "stopped", reason);
|
|
1121
|
+
return run;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Живой прогон-резолвер конфликта для review-прогона #id (если есть). «Живой» =
|
|
1125
|
+
* не в терминальном статусе: пока такой в работе/на ревью — второго резолвера на тот
|
|
1126
|
+
* же PR не поднимаем (иначе на каждый тик плодили бы дронов). stalled/failed/done —
|
|
1127
|
+
* не живые: резолвер сорвался/добил → можно, если не исчёрпан лимит попыток.
|
|
1128
|
+
*/
|
|
1129
|
+
activeResolverFor(reviewRunId) {
|
|
1130
|
+
const row = this.db.prepare(
|
|
1131
|
+
`SELECT * FROM runs WHERE resolves_run_id = ? AND status NOT IN (${RESOLVER_TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
|
|
1132
|
+
).get(reviewRunId);
|
|
1133
|
+
return row ? rowToRun(row) : null;
|
|
1134
|
+
}
|
|
1135
|
+
/** Сколько всего резолверов уже поднимали на этот review-PR — потолок авто-попыток. */
|
|
1136
|
+
resolverCountFor(reviewRunId) {
|
|
1137
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
|
|
1138
|
+
return row.n;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Сколько ЖИВЫХ/УСПЕШНЫХ ревьюеров уже поднимали на этот review-PR В ТЕКУЩЕМ ЦИКЛЕ
|
|
1142
|
+
* (авто-ревью, TASWM-148; скоуп по циклу — авто-ревью v2, TASWM-162) — one-shot
|
|
1143
|
+
* идемпотентность БЕЗ лимита попыток (в отличие от резолвера): >0 → в этом цикле ревьюер
|
|
1144
|
+
* уже есть, повторно не спавним. failed/stalled ревьюеры (worktree/launch сорвались ДО
|
|
1145
|
+
* вердикта) в счёт НЕ идут — иначе один транзиентный сбой спавна (диск/git/gh) навсегда
|
|
1146
|
+
* блокировал бы ревью цикла без шанса на повтор. stopped/superseded — тоже НЕ считаем
|
|
1147
|
+
* (TASWM-253): stopped — оператор закрыл зомби-ревьюера руками (POST /swarm/runs/cleanup-
|
|
1148
|
+
* dead), superseded — оператор явно сбросил попытки ревью (POST /swarm/review/retry,
|
|
1149
|
+
* см. packages/api/src/swarm/routes.ts); оба варианта означают «эта попытка не в счёт»,
|
|
1150
|
+
* а не «ревьюер ещё жив». РАНЬШЕ stopped
|
|
1151
|
+
* тут не исключался — операторский kill зомби-ревьюера навсегда держал гейт закрытым
|
|
1152
|
+
* (reviewerCountFor>0 бесконечно), хотя счётчик попыток (ниже) уже не рос. Скоуп «текущий
|
|
1153
|
+
* цикл» = review_cycle ревьюера равен review_cycle проверяемого прогона: после фикс-
|
|
1154
|
+
* итерации исполнитель бампит цикл (droneServer.done), ревьюеры прошлого цикла отваливаются
|
|
1155
|
+
* из счёта → гейт снова открыт для ре-ревьюера (без него бейдж «changes» держал бы слот
|
|
1156
|
+
* навсегда).
|
|
1157
|
+
*/
|
|
1158
|
+
reviewerCountFor(reviewRunId) {
|
|
1159
|
+
const row = this.db.prepare(
|
|
1160
|
+
"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)"
|
|
1161
|
+
).get(reviewRunId, reviewRunId);
|
|
1162
|
+
return row.n;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
|
|
1166
|
+
* ВСЕГО (любой статус, включая failed/stalled/stopped — специально в отличие от
|
|
1167
|
+
* reviewerCountFor выше) поднималось на review-прогон #reviewRunId В ТЕКУЩЕМ ЦИКЛЕ (скоуп
|
|
1168
|
+
* по циклу — авто-ревью v2, TASWM-162). Зеркалит resolverCountFor — плоский потолок
|
|
1169
|
+
* попыток на цикл. Именно это ловит карусель: reviewerCountFor исключает failed/stalled
|
|
1170
|
+
* (чтобы ОДИН транзиентный сбой не блокировал ревью), но тем самым допускает бесконечный
|
|
1171
|
+
* ретрай, если ревьюер стабильно умирает без done(). Скоуп по циклу — чтобы кап считался
|
|
1172
|
+
* заново на каждой фикс-итерации, а не суммировал попытки прошлых (успешно завершённых)
|
|
1173
|
+
* циклов. superseded — ЕДИНСТВЕННОЕ исключение из капа (TASWM-253): это статус, которым
|
|
1174
|
+
* POST /swarm/review/retry помечает попытки при операторском «попробовать ревью заново» —
|
|
1175
|
+
* без исключения сброс не мог бы ничего сбросить (COUNT(*) слеп к статусу). failed/
|
|
1176
|
+
* stalled/stopped по-прежнему считаются — иначе кап потерял бы смысл на органических
|
|
1177
|
+
* смертях (ровно то, для чего его вводили в TASWM-204).
|
|
1178
|
+
*/
|
|
1179
|
+
reviewerAttemptCountFor(reviewRunId) {
|
|
1180
|
+
const row = this.db.prepare(
|
|
1181
|
+
"SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status != 'superseded' AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0)"
|
|
1182
|
+
).get(reviewRunId, reviewRunId);
|
|
1183
|
+
return row.n;
|
|
1184
|
+
}
|
|
1185
|
+
/** Последняя (любой статус) попытка ревьюера ТЕКУЩЕГО ЦИКЛА на review-прогон #reviewRunId
|
|
1186
|
+
* (TASWM-196; скоуп по циклу — TASWM-162) — см. store.ts:lastReviewerAttemptFor. */
|
|
1187
|
+
lastReviewerAttemptFor(reviewRunId) {
|
|
1188
|
+
const row = this.db.prepare(
|
|
1189
|
+
"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"
|
|
1190
|
+
).get(reviewRunId, reviewRunId);
|
|
1191
|
+
return row ? rowToRun(row) : null;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
|
|
1195
|
+
* closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
|
|
1196
|
+
* не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
|
|
1197
|
+
*/
|
|
1198
|
+
activeReviewersFor(reviewRunId) {
|
|
1199
|
+
const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
|
|
1200
|
+
return rows.map(rowToRun);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Последний живой (не терминальный) прогон по slug — для swarm cancel. Служебные
|
|
1204
|
+
* сабраны (reviews_run_id/resolves_run_id — ревьюер/резолвер, TASWM-273) исключены:
|
|
1205
|
+
* они заводятся с ТЕМ ЖЕ task_slug, что и родительский прогон, и всегда бо́льшим id
|
|
1206
|
+
* (спавнятся позже) — без исключения `ORDER BY id DESC LIMIT 1` подсовывал бы
|
|
1207
|
+
* cancelRun паузнутого ревьюера (paused НЕ терминален) вместо реального прогона,
|
|
1208
|
+
* которого целился остановить оператор/`swarm cancel <slug>`: безусловный removeWorktree
|
|
1209
|
+
* в cancelRun сносил бы worktree ревьюера, ждущего CI, и переводил его в stopped мимо
|
|
1210
|
+
* обычной цепочки паузы/побудки — найдено при разборе TASWM-273 (PR #564 завис в stalled
|
|
1211
|
+
* без вердикта: worktree ревьюера пропал так, что статус до этого не менялся). Сабран
|
|
1212
|
+
* отменяют через свой id, не через slug — swarm cancel всегда целится в родительский прогон.
|
|
1213
|
+
*/
|
|
1214
|
+
activeRunForSlug(slug) {
|
|
1215
|
+
const row = this.db.prepare(
|
|
1216
|
+
`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`
|
|
1217
|
+
).get(slug);
|
|
1218
|
+
return row ? rowToRun(row) : null;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Зоны, занятые сейчас работающими дронами — для конфликт-осознанной раздачи.
|
|
1222
|
+
* С repo — только в этом репозитории (одна area в разных репо не конфликтует).
|
|
1223
|
+
* Без repo — по всему рою (обратная совместимость).
|
|
1224
|
+
*/
|
|
1225
|
+
runningAreas(repo) {
|
|
1226
|
+
const base3 = "SELECT DISTINCT area FROM runs WHERE status IN ('running','blocked') AND area IS NOT NULL";
|
|
1227
|
+
const rows = repo === void 0 ? this.db.prepare(base3).all() : this.db.prepare(`${base3} AND repo = ?`).all(repo);
|
|
1228
|
+
return rows.map((r) => r.area);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
|
|
1232
|
+
* статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
|
|
1233
|
+
* (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
|
|
1234
|
+
* а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
|
|
1235
|
+
* Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
|
|
1236
|
+
* но очередь не виснет (гейт — пропуск в цикле, не блокировка).
|
|
1237
|
+
*/
|
|
1238
|
+
unmetDeps(depends_on) {
|
|
1239
|
+
if (!depends_on || depends_on.length === 0) return [];
|
|
1240
|
+
const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
|
|
1241
|
+
const done = new Set(rows.map((r) => r.task_slug));
|
|
1242
|
+
return depends_on.filter((slug) => !done.has(slug));
|
|
1243
|
+
}
|
|
1244
|
+
/** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
|
|
1245
|
+
distinctRepos() {
|
|
1246
|
+
const rows = this.db.prepare("SELECT DISTINCT repo FROM runs WHERE repo IS NOT NULL AND repo <> ''").all();
|
|
1247
|
+
return rows.map((r) => r.repo);
|
|
1248
|
+
}
|
|
1249
|
+
// --- импорт истории из другой БД (объединение роёв) ---
|
|
1250
|
+
/**
|
|
1251
|
+
* Идемпотентный перенос runs/events/questions из другой swarm-БД в эту.
|
|
1252
|
+
* id прогонов ремапятся (старый→новый); run_id в events/questions и superseded_by
|
|
1253
|
+
* в runs правятся по карте; всем импортируемым прогонам проставляется repo (норм.).
|
|
1254
|
+
* pid НЕ переносим — чужой pid в объединённой БД бессмыслен и мог бы совпасть с
|
|
1255
|
+
* живым процессом (ложный liveness); импортируемые прогоны исторические.
|
|
1256
|
+
* Живые статусы нормализуем (running/blocked→stalled, queued→stopped) и открытые
|
|
1257
|
+
* вопросы не тащим — иначе импорт даёт вечных зомби: без pid death-detect их
|
|
1258
|
+
* пропускает, они навсегда держат (repo,area)-лок и «в работе» в метриках, а их
|
|
1259
|
+
* открытые вопросы «воскрешают» архивный прогон через answer.
|
|
1260
|
+
* Повторный вызов на том же source — no-op (маркер в таблице imports). Source
|
|
1261
|
+
* открываем на ЗАПИСЬ (importDb только ЧИТАЕТ из него): все БД роя — WAL, а bun:sqlite
|
|
1262
|
+
* с readonly НЕ открывает WAL-source без -shm (реальный перенос обычно = копия одного
|
|
1263
|
+
* файла swarm.db без sidecar'ов → SQLITE_CANTOPEN). Несуществующий путь ловим existsSync
|
|
1264
|
+
* ДО открытия — честная ошибка, а не пустой файл (rw создал бы его). Всё в одной
|
|
1265
|
+
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
1266
|
+
*/
|
|
1267
|
+
importDb(sourcePath, repo) {
|
|
1268
|
+
const abs = resolve2(sourcePath);
|
|
1269
|
+
const repoN = normalizeRepo(repo);
|
|
1270
|
+
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
1271
|
+
if (already) return { imported: false, runs: already.run_count };
|
|
1272
|
+
if (!existsSync8(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
1273
|
+
const src = new this.databaseCtor(abs);
|
|
1274
|
+
try {
|
|
1275
|
+
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
1276
|
+
const events = src.prepare("SELECT * FROM events ORDER BY id").all();
|
|
1277
|
+
const questions = src.prepare("SELECT * FROM questions ORDER BY id").all();
|
|
1278
|
+
const txn = this.db.transaction(() => {
|
|
1279
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1280
|
+
const insRun = this.db.prepare(
|
|
1281
|
+
"INSERT INTO runs (task_slug, repo, worktree_path, branch, status, area, model, cost_usd, pr_url, decision, stop_reason, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
1282
|
+
);
|
|
1283
|
+
for (const r of runs) {
|
|
1284
|
+
const raw = r.status ?? "done";
|
|
1285
|
+
let status = raw;
|
|
1286
|
+
let stopReason = r.stop_reason ?? null;
|
|
1287
|
+
let normEvent = null;
|
|
1288
|
+
if (raw === "running" || raw === "blocked") {
|
|
1289
|
+
status = "stalled";
|
|
1290
|
+
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" };
|
|
1291
|
+
} else if (raw === "queued") {
|
|
1292
|
+
status = "stopped";
|
|
1293
|
+
stopReason = "\u0430\u0440\u0445\u0438\u0432\u043D\u044B\u0439 \u0438\u043C\u043F\u043E\u0440\u0442";
|
|
1294
|
+
normEvent = { kind: "stopped", text: "\u0430\u0440\u0445\u0438\u0432\u043D\u044B\u0439 \u0438\u043C\u043F\u043E\u0440\u0442" };
|
|
1295
|
+
}
|
|
1296
|
+
const res = insRun.run(
|
|
1297
|
+
r.task_slug,
|
|
1298
|
+
repoN,
|
|
1299
|
+
r.worktree_path ?? null,
|
|
1300
|
+
r.branch ?? null,
|
|
1301
|
+
status,
|
|
1302
|
+
r.area ?? null,
|
|
1303
|
+
r.model ?? null,
|
|
1304
|
+
r.cost_usd ?? 0,
|
|
1305
|
+
r.pr_url ?? null,
|
|
1306
|
+
r.decision ?? null,
|
|
1307
|
+
stopReason,
|
|
1308
|
+
r.created_at ?? null,
|
|
1309
|
+
r.updated_at ?? null
|
|
1310
|
+
);
|
|
1311
|
+
const newId = Number(res.lastInsertRowid);
|
|
1312
|
+
idMap.set(r.id, newId);
|
|
1313
|
+
if (normEvent) this.addEvent(newId, normEvent.kind, normEvent.text);
|
|
1314
|
+
}
|
|
1315
|
+
const setSup = this.db.prepare("UPDATE runs SET superseded_by = ? WHERE id = ?");
|
|
1316
|
+
for (const r of runs) {
|
|
1317
|
+
const sup = r.superseded_by;
|
|
1318
|
+
if (sup != null && idMap.has(sup)) setSup.run(idMap.get(sup), idMap.get(r.id));
|
|
1319
|
+
}
|
|
1320
|
+
const insEv = this.db.prepare("INSERT INTO events (run_id, kind, text, actor, at) VALUES (?,?,?,?,?)");
|
|
1321
|
+
for (const e of events) {
|
|
1322
|
+
const oldRun = e.run_id;
|
|
1323
|
+
const newRun = oldRun != null ? idMap.get(oldRun) ?? null : null;
|
|
1324
|
+
insEv.run(newRun, e.kind, e.text ?? null, e.actor ?? null, e.at ?? null);
|
|
1325
|
+
}
|
|
1326
|
+
const insQ = this.db.prepare(
|
|
1327
|
+
"INSERT INTO questions (run_id, text, options, status, answer, answered_by, escalated, asked_at, answered_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
|
1328
|
+
);
|
|
1329
|
+
for (const q of questions) {
|
|
1330
|
+
if (q.status === "open") continue;
|
|
1331
|
+
const newRun = idMap.get(q.run_id);
|
|
1332
|
+
if (newRun == null) continue;
|
|
1333
|
+
insQ.run(
|
|
1334
|
+
newRun,
|
|
1335
|
+
q.text,
|
|
1336
|
+
q.options ?? null,
|
|
1337
|
+
q.status ?? "answered",
|
|
1338
|
+
q.answer ?? null,
|
|
1339
|
+
q.answered_by ?? null,
|
|
1340
|
+
q.escalated ?? 0,
|
|
1341
|
+
q.asked_at ?? null,
|
|
1342
|
+
q.answered_at ?? null
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
this.db.prepare("INSERT INTO imports (source, run_count) VALUES (?, ?)").run(abs, runs.length);
|
|
1346
|
+
});
|
|
1347
|
+
txn();
|
|
1348
|
+
return { imported: true, runs: runs.length };
|
|
1349
|
+
} finally {
|
|
1350
|
+
src.close();
|
|
1183
1351
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1352
|
+
}
|
|
1353
|
+
// --- questions (петля ask/answer) ---
|
|
1354
|
+
ask(run_id, text, options) {
|
|
1355
|
+
const insert = () => this.db.prepare("INSERT INTO questions (run_id, text, options) VALUES (?, ?, ?)").run(run_id, text, options && options.length ? JSON.stringify(options) : null);
|
|
1356
|
+
let res;
|
|
1357
|
+
try {
|
|
1358
|
+
res = insert();
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
if (isForeignKeyError(err)) {
|
|
1361
|
+
console.error(
|
|
1362
|
+
`[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.`
|
|
1363
|
+
);
|
|
1364
|
+
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1365
|
+
res = insert();
|
|
1366
|
+
} else {
|
|
1367
|
+
throw err;
|
|
1368
|
+
}
|
|
1189
1369
|
}
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1370
|
+
const id = Number(res.lastInsertRowid);
|
|
1371
|
+
this.updateRun(run_id, { status: "blocked" });
|
|
1372
|
+
this.addEvent(run_id, "blocked", text);
|
|
1373
|
+
return this.getQuestion(id);
|
|
1374
|
+
}
|
|
1375
|
+
/** Оркестратор поднял вопрос человеку (техническое решил бы сам). */
|
|
1376
|
+
escalateQuestion(id) {
|
|
1377
|
+
this.db.prepare("UPDATE questions SET escalated=1 WHERE id=?").run(id);
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Снять открытый вопрос без реального ответа (дрон замер: overmind недоступен,
|
|
1381
|
+
* ответа не будет — TASWM-41). Отдельный статус 'withdrawn' (не 'answered'): вопрос
|
|
1382
|
+
* уходит из openQuestions() (WHERE status='open'), защищён от answer() (тот правит
|
|
1383
|
+
* только open И воскрешает прогон лишь на статусе 'answered') — так снятый вопрос
|
|
1384
|
+
* не оживит замерший прогон. Остаётся в истории как снятый с пояснением.
|
|
1385
|
+
*/
|
|
1386
|
+
withdrawQuestion(id, reason) {
|
|
1387
|
+
this.db.prepare("UPDATE questions SET status='withdrawn', answer=?, answered_at=datetime('now') WHERE id=? AND status='open'").run(reason, id);
|
|
1388
|
+
}
|
|
1389
|
+
answer(question_id, answer, by = "human") {
|
|
1390
|
+
this.db.prepare(
|
|
1391
|
+
"UPDATE questions SET status='answered', answer=?, answered_by=?, answered_at=datetime('now') WHERE id=? AND status='open'"
|
|
1392
|
+
).run(answer, by, question_id);
|
|
1393
|
+
const q = this.getQuestion(question_id);
|
|
1394
|
+
if (q && q.status === "answered") {
|
|
1395
|
+
this.updateRun(q.run_id, { status: "running" });
|
|
1396
|
+
this.addEvent(q.run_id, "answered", answer, by);
|
|
1208
1397
|
}
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
} catch (err) {
|
|
1224
|
-
if (isForeignKeyError(err)) {
|
|
1225
|
-
console.error(
|
|
1226
|
-
`[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.`
|
|
1398
|
+
return q;
|
|
1399
|
+
}
|
|
1400
|
+
/** Пометить старый прогон переигранным на новый (откат на другой вариант). */
|
|
1401
|
+
supersede(oldId, newId, by = "human") {
|
|
1402
|
+
this.updateRun(oldId, { superseded_by: newId });
|
|
1403
|
+
this.addEvent(oldId, "superseded", `\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u2192 #${newId}`, by);
|
|
1404
|
+
}
|
|
1405
|
+
getQuestion(id) {
|
|
1406
|
+
const row = this.db.prepare("SELECT * FROM questions WHERE id = ?").get(id);
|
|
1407
|
+
return row ? rowToQuestion(row) : null;
|
|
1408
|
+
}
|
|
1409
|
+
openQuestions() {
|
|
1410
|
+
return this.db.prepare("SELECT * FROM questions WHERE status='open' ORDER BY id").all().map(
|
|
1411
|
+
rowToQuestion
|
|
1227
1412
|
);
|
|
1228
|
-
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1229
|
-
res = insert();
|
|
1230
|
-
} else {
|
|
1231
|
-
throw err;
|
|
1232
1413
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
return this.getQuestion(id);
|
|
1238
|
-
}
|
|
1239
|
-
/** Оркестратор поднял вопрос человеку (техническое решил бы сам). */
|
|
1240
|
-
escalateQuestion(id) {
|
|
1241
|
-
this.db.prepare("UPDATE questions SET escalated=1 WHERE id=?").run(id);
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* Снять открытый вопрос без реального ответа (дрон замер: overmind недоступен,
|
|
1245
|
-
* ответа не будет — TASWM-41). Отдельный статус 'withdrawn' (не 'answered'): вопрос
|
|
1246
|
-
* уходит из openQuestions() (WHERE status='open'), защищён от answer() (тот правит
|
|
1247
|
-
* только open И воскрешает прогон лишь на статусе 'answered') — так снятый вопрос
|
|
1248
|
-
* не оживит замерший прогон. Остаётся в истории как снятый с пояснением.
|
|
1249
|
-
*/
|
|
1250
|
-
withdrawQuestion(id, reason) {
|
|
1251
|
-
this.db.prepare("UPDATE questions SET status='withdrawn', answer=?, answered_at=datetime('now') WHERE id=? AND status='open'").run(reason, id);
|
|
1252
|
-
}
|
|
1253
|
-
answer(question_id, answer, by = "human") {
|
|
1254
|
-
this.db.prepare(
|
|
1255
|
-
"UPDATE questions SET status='answered', answer=?, answered_by=?, answered_at=datetime('now') WHERE id=? AND status='open'"
|
|
1256
|
-
).run(answer, by, question_id);
|
|
1257
|
-
const q = this.getQuestion(question_id);
|
|
1258
|
-
if (q && q.status === "answered") {
|
|
1259
|
-
this.updateRun(q.run_id, { status: "running" });
|
|
1260
|
-
this.addEvent(q.run_id, "answered", answer, by);
|
|
1261
|
-
}
|
|
1262
|
-
return q;
|
|
1263
|
-
}
|
|
1264
|
-
/** Пометить старый прогон переигранным на новый (откат на другой вариант). */
|
|
1265
|
-
supersede(oldId, newId, by = "human") {
|
|
1266
|
-
this.updateRun(oldId, { superseded_by: newId });
|
|
1267
|
-
this.addEvent(oldId, "superseded", `\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u2192 #${newId}`, by);
|
|
1268
|
-
}
|
|
1269
|
-
getQuestion(id) {
|
|
1270
|
-
const row = this.db.prepare("SELECT * FROM questions WHERE id = ?").get(id);
|
|
1271
|
-
return row ? rowToQuestion(row) : null;
|
|
1272
|
-
}
|
|
1273
|
-
openQuestions() {
|
|
1274
|
-
return this.db.prepare("SELECT * FROM questions WHERE status='open' ORDER BY id").all().map(
|
|
1275
|
-
rowToQuestion
|
|
1276
|
-
);
|
|
1277
|
-
}
|
|
1278
|
-
/** ВСЕ вопросы прогона (любого статуса), по возрастанию id — см. SwarmStore.questionsFor. */
|
|
1279
|
-
questionsFor(run_id) {
|
|
1280
|
-
return this.db.prepare("SELECT * FROM questions WHERE run_id = ? ORDER BY id").all(run_id).map(
|
|
1281
|
-
rowToQuestion
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1284
|
-
/** id прогонов, у которых есть хотя бы один вопрос — доступность «Переиграть» в модалке. */
|
|
1285
|
-
runIdsWithQuestions() {
|
|
1286
|
-
return this.db.prepare("SELECT DISTINCT run_id FROM questions").all().map((r) => r.run_id);
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* Открытые вопросы дронов отряда Героя #heroId (join questions×runs по hero_run_id).
|
|
1290
|
-
* Герой видит их в squad_status и отвечает через answer_drone; вопрос не из отряда сюда
|
|
1291
|
-
* не попадёт — это и есть гейт «отвечать только своим» (TASWM-81).
|
|
1292
|
-
*/
|
|
1293
|
-
openQuestionsFor(heroId) {
|
|
1294
|
-
return this.db.prepare(
|
|
1295
|
-
"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"
|
|
1296
|
-
).all(heroId).map(rowToQuestion);
|
|
1297
|
-
}
|
|
1298
|
-
// --- events (лог/хроника) ---
|
|
1299
|
-
addEvent(run_id, kind, text, actor = null) {
|
|
1300
|
-
const clean = sanitizeEventText(text);
|
|
1301
|
-
const insert = () => this.db.prepare("INSERT INTO events (run_id, kind, text, actor) VALUES (?, ?, ?, ?)").run(run_id, kind, clean, actor);
|
|
1302
|
-
try {
|
|
1303
|
-
insert();
|
|
1304
|
-
} catch (err) {
|
|
1305
|
-
if (run_id != null && isForeignKeyError(err)) {
|
|
1306
|
-
console.error(
|
|
1307
|
-
`[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.`
|
|
1414
|
+
/** ВСЕ вопросы прогона (любого статуса), по возрастанию id — см. SwarmStore.questionsFor. */
|
|
1415
|
+
questionsFor(run_id) {
|
|
1416
|
+
return this.db.prepare("SELECT * FROM questions WHERE run_id = ? ORDER BY id").all(run_id).map(
|
|
1417
|
+
rowToQuestion
|
|
1308
1418
|
);
|
|
1309
|
-
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1310
|
-
insert();
|
|
1311
|
-
return;
|
|
1312
1419
|
}
|
|
1313
|
-
|
|
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
|
-
|
|
1420
|
+
/** id прогонов, у которых есть хотя бы один вопрос — доступность «Переиграть» в модалке. */
|
|
1421
|
+
runIdsWithQuestions() {
|
|
1422
|
+
return this.db.prepare("SELECT DISTINCT run_id FROM questions").all().map((r) => r.run_id);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Открытые вопросы дронов отряда Героя #heroId (join questions×runs по hero_run_id).
|
|
1426
|
+
* Герой видит их в squad_status и отвечает через answer_drone; вопрос не из отряда сюда
|
|
1427
|
+
* не попадёт — это и есть гейт «отвечать только своим» (TASWM-81).
|
|
1428
|
+
*/
|
|
1429
|
+
openQuestionsFor(heroId) {
|
|
1430
|
+
return this.db.prepare(
|
|
1431
|
+
"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"
|
|
1432
|
+
).all(heroId).map(rowToQuestion);
|
|
1433
|
+
}
|
|
1434
|
+
// --- events (лог/хроника) ---
|
|
1435
|
+
addEvent(run_id, kind, text, actor = null) {
|
|
1436
|
+
const clean = sanitizeEventText(text);
|
|
1437
|
+
const insert = () => this.db.prepare("INSERT INTO events (run_id, kind, text, actor) VALUES (?, ?, ?, ?)").run(run_id, kind, clean, actor);
|
|
1438
|
+
try {
|
|
1439
|
+
insert();
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
if (run_id != null && isForeignKeyError(err)) {
|
|
1442
|
+
console.error(
|
|
1443
|
+
`[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.`
|
|
1444
|
+
);
|
|
1445
|
+
this.ensureRun({ id: run_id, task_slug: "?" });
|
|
1446
|
+
insert();
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
throw err;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
events(run_id) {
|
|
1453
|
+
return this.db.prepare("SELECT * FROM events WHERE run_id = ? ORDER BY id").all(run_id);
|
|
1454
|
+
}
|
|
1455
|
+
/** Хроника роя (вкладка История): Q&A (вопрос+варианты+ответ+автор) + вехи прогонов, новые сверху. */
|
|
1456
|
+
history(limit = 50) {
|
|
1457
|
+
const items = [];
|
|
1458
|
+
const name = (row) => row.name ?? `#${row.run_id}`;
|
|
1459
|
+
for (const row of this.db.prepare(
|
|
1460
|
+
"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"
|
|
1461
|
+
).all()) {
|
|
1462
|
+
const q = rowToQuestion(row);
|
|
1463
|
+
items.push({
|
|
1464
|
+
kind: "qa",
|
|
1465
|
+
at: row.answered_at ?? q.asked_at,
|
|
1466
|
+
slug: row.slug ?? "?",
|
|
1467
|
+
name: name(row),
|
|
1468
|
+
repo: row.repo ?? "",
|
|
1469
|
+
area: row.area ?? null,
|
|
1470
|
+
qid: q.id,
|
|
1471
|
+
question: q.text,
|
|
1472
|
+
options: q.options,
|
|
1473
|
+
answer: q.answer,
|
|
1474
|
+
answered_by: q.answered_by,
|
|
1475
|
+
escalated: q.escalated,
|
|
1476
|
+
open: q.status === "open",
|
|
1477
|
+
title: row.title ?? null,
|
|
1478
|
+
pr: row.pr ?? null,
|
|
1479
|
+
taskUrl: row.taskUrl ?? null
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
for (const row of this.db.prepare(
|
|
1483
|
+
"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"
|
|
1484
|
+
).all()) {
|
|
1485
|
+
if (!isMilestoneKind(row.kind)) continue;
|
|
1486
|
+
const global = row.run_id == null;
|
|
1487
|
+
items.push({
|
|
1488
|
+
kind: "milestone",
|
|
1489
|
+
at: row.at,
|
|
1490
|
+
slug: row.slug ?? (global ? "\u0440\u043E\u0439" : "?"),
|
|
1491
|
+
name: global ? "\u0440\u043E\u0439" : name(row),
|
|
1492
|
+
repo: row.repo ?? "",
|
|
1493
|
+
event: row.kind,
|
|
1494
|
+
text: row.text ?? null,
|
|
1495
|
+
actor: row.actor ?? null,
|
|
1496
|
+
title: row.title ?? null,
|
|
1497
|
+
pr: row.pr ?? null,
|
|
1498
|
+
taskUrl: row.taskUrl ?? null
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
items.sort((a, b) => a.at < b.at ? 1 : a.at > b.at ? -1 : 0);
|
|
1502
|
+
return items.slice(0, limit);
|
|
1503
|
+
}
|
|
1504
|
+
/** Сколько PR смержено сегодня (UTC) — для метрики «Смержено сегодня» на Главной. */
|
|
1505
|
+
mergedToday() {
|
|
1506
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM events WHERE kind = 'merged' AND date(at) = date('now')").get();
|
|
1507
|
+
return row.n;
|
|
1508
|
+
}
|
|
1509
|
+
// --- штормовой режим (TASWM-23): состояние роя в событиях (source of truth) ---
|
|
1510
|
+
/**
|
|
1511
|
+
* В шторме ли рой сейчас: смотрим последнее ГЛОБАЛЬНОЕ (run_id NULL) событие
|
|
1512
|
+
* storm_start/storm_end. storm_start без последующего storm_end → шторм активен.
|
|
1513
|
+
* Выводится из событий → переживает рестарт overmind.
|
|
1514
|
+
*/
|
|
1515
|
+
stormActive() {
|
|
1516
|
+
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();
|
|
1517
|
+
return row?.kind === "storm_start";
|
|
1518
|
+
}
|
|
1519
|
+
/** Время начала ТЕКУЩЕГО шторма (at события storm_start) или null, если шторма нет. */
|
|
1520
|
+
currentStormStartedAt() {
|
|
1521
|
+
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();
|
|
1522
|
+
return row?.kind === "storm_start" ? row.at : null;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Время ПОСЛЕДНЕГО отбоя (at события storm_end) или null, если шторм сейчас идёт /
|
|
1526
|
+
* отбоя ещё не было. Для гистерезиса TASWM-208: свежий отбой держит кулдаун на
|
|
1527
|
+
* эвристический перезапуск. Смотрим то же последнее storm_start/storm_end, что и
|
|
1528
|
+
* stormActive/currentStormStartedAt — probe-события не участвуют.
|
|
1529
|
+
*/
|
|
1530
|
+
lastStormEndedAt() {
|
|
1531
|
+
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();
|
|
1532
|
+
return row?.kind === "storm_end" ? row.at : null;
|
|
1533
|
+
}
|
|
1534
|
+
/** Пометить вход в шторм (глобальное событие). Идемпотентность — на стороне вызывающего. */
|
|
1535
|
+
startStorm(reason) {
|
|
1536
|
+
this.addEvent(null, "storm_start", reason);
|
|
1537
|
+
}
|
|
1538
|
+
/** Пометить отбой шторма (глобальное событие). */
|
|
1539
|
+
endStorm(reason) {
|
|
1540
|
+
this.addEvent(null, "storm_end", reason);
|
|
1541
|
+
}
|
|
1542
|
+
/** Записать смерть работающего дрона — семпл для скользящего окна детекции шторма. */
|
|
1543
|
+
recordDeath(runId, note) {
|
|
1544
|
+
this.addEvent(runId, "died", note ?? "\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 (dead pid)");
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Смертей дронов (событий died) с момента cutoff. cutoffSeconds — epoch-СЕКУНДЫ;
|
|
1548
|
+
* datetime(?, 'unixepoch') форматирует его тем же «YYYY-MM-DD HH:MM:SS» UTC, что и
|
|
1549
|
+
* datetime('now') в at — строковое сравнение корректно, без парса на стороне JS.
|
|
1550
|
+
*/
|
|
1551
|
+
deathCountSince(cutoffSeconds) {
|
|
1552
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM events WHERE kind='died' AND at >= datetime(?, 'unixepoch')").get(cutoffSeconds);
|
|
1553
|
+
return row.n;
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Спавн-фейлов (событий failed на НЕ-резолверных/НЕ-ревьюерных прогонах) с момента
|
|
1557
|
+
* cutoff. Резолверные (merge-main-in) и ревьюерные (авто-ревью, TASWM-148) failed не
|
|
1558
|
+
* считаем штормовым сигналом.
|
|
1559
|
+
*/
|
|
1560
|
+
spawnFailCountSince(cutoffSeconds) {
|
|
1561
|
+
const row = this.db.prepare(
|
|
1562
|
+
"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')"
|
|
1563
|
+
).get(cutoffSeconds);
|
|
1564
|
+
return row.n;
|
|
1565
|
+
}
|
|
1566
|
+
/** Записать результат периодической проверки шторма (health-probe + внешний сигнал). */
|
|
1567
|
+
recordStormProbe(probe, ext, reason, resetHint) {
|
|
1568
|
+
this.addEvent(null, "storm_probe", encodeProbe({ probe, ext, reason: reason ?? null, resetHint: resetHint ?? null }));
|
|
1569
|
+
}
|
|
1570
|
+
/** Последняя запись storm_probe (для троттлинга по времени и последнего внешнего сигнала). */
|
|
1571
|
+
latestStormProbe() {
|
|
1572
|
+
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();
|
|
1573
|
+
return row ? { rec: decodeProbe(row.text), at: row.at } : null;
|
|
1574
|
+
}
|
|
1575
|
+
/** Записи storm_probe текущего шторма (at >= sinceIso), по возрастанию — для счётчика успехов подряд. */
|
|
1576
|
+
stormProbeRecordsSince(sinceIso) {
|
|
1577
|
+
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);
|
|
1578
|
+
return rows.map((r) => decodeProbe(r.text));
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Последняя РЕАЛЬНАЯ health-probe (probe !== null; ext-only записи пропускаем) с её
|
|
1582
|
+
* временем. TASWM-137 п.д: подтверждающая проба перед входом в шторм по эвристике —
|
|
1583
|
+
* зелёная проба вето́ит ложный шторм; свежую (в пределах probeIntervalMs) переиспользуем,
|
|
1584
|
+
* не гоняя новую каждый тик. Смотрим только хвост (LIMIT) — старее нам всё равно не нужно.
|
|
1585
|
+
*/
|
|
1586
|
+
lastHealthProbe() {
|
|
1587
|
+
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();
|
|
1588
|
+
for (const r of rows) {
|
|
1589
|
+
const rec = decodeProbe(r.text);
|
|
1590
|
+
if (rec.probe !== null) return { ok: rec.probe, at: r.at };
|
|
1591
|
+
}
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1457
1595
|
}
|
|
1458
|
-
};
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// src/cli.ts
|
|
1599
|
+
import { basename as basename4 } from "path";
|
|
1600
|
+
import { existsSync as existsSync9, readdirSync as readdirSync6, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
1601
|
+
import { createInterface } from "readline/promises";
|
|
1602
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
1459
1603
|
|
|
1460
1604
|
// src/swarmClient.ts
|
|
1461
|
-
|
|
1605
|
+
init_config();
|
|
1606
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1462
1607
|
import { dirname } from "path";
|
|
1463
1608
|
import { randomUUID } from "crypto";
|
|
1464
1609
|
|
|
@@ -1477,6 +1622,7 @@ function execQuiet(file, args = [], opts = {}) {
|
|
|
1477
1622
|
var PROC_SNAPSHOT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
1478
1623
|
|
|
1479
1624
|
// src/resources.ts
|
|
1625
|
+
init_config();
|
|
1480
1626
|
import { readFileSync as readFileSync2, statfsSync } from "fs";
|
|
1481
1627
|
function isTransientResourceError(e) {
|
|
1482
1628
|
const code = e?.code;
|
|
@@ -1618,13 +1764,18 @@ function hostResourceSnapshot() {
|
|
|
1618
1764
|
};
|
|
1619
1765
|
}
|
|
1620
1766
|
|
|
1767
|
+
// src/swarmClient.ts
|
|
1768
|
+
init_names2();
|
|
1769
|
+
init_storm();
|
|
1770
|
+
|
|
1621
1771
|
// src/logTail.ts
|
|
1622
|
-
import { closeSync, existsSync
|
|
1772
|
+
import { closeSync, existsSync, openSync, readSync, readdirSync, statSync } from "fs";
|
|
1623
1773
|
import { join as join2 } from "path";
|
|
1624
1774
|
var TAIL_LINES_FALLBACK = 200;
|
|
1625
1775
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
1626
1776
|
|
|
1627
1777
|
// src/swarmClient.ts
|
|
1778
|
+
init_src();
|
|
1628
1779
|
var RESOLVER_TERMINAL = /* @__PURE__ */ new Set([...TERMINAL_STATUSES, "stalled"]);
|
|
1629
1780
|
var SLUG_TERMINAL = new Set(TERMINAL_STATUSES);
|
|
1630
1781
|
function runRef(runKey) {
|
|
@@ -1720,6 +1871,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
1720
1871
|
flushing = false;
|
|
1721
1872
|
flushTimer = null;
|
|
1722
1873
|
backoffMs = 500;
|
|
1874
|
+
/** Сколько раз пробуем отвергнутую запись, прежде чем выбросить её (TASWM-338). */
|
|
1875
|
+
rejected = /* @__PURE__ */ new Map();
|
|
1876
|
+
static REJECT_LIMIT = 5;
|
|
1723
1877
|
static BACKOFF_BASE = 500;
|
|
1724
1878
|
static BACKOFF_MAX = 3e4;
|
|
1725
1879
|
constructor(opts) {
|
|
@@ -1731,7 +1885,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1731
1885
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
1732
1886
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
1733
1887
|
this.host = opts.host ?? null;
|
|
1734
|
-
this.repoExists = opts.repoExists ??
|
|
1888
|
+
this.repoExists = opts.repoExists ?? existsSync2;
|
|
1735
1889
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
1736
1890
|
this.outboxPath = opts.outboxPath;
|
|
1737
1891
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
@@ -1741,14 +1895,14 @@ var SwarmClient = class _SwarmClient {
|
|
|
1741
1895
|
}
|
|
1742
1896
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
1743
1897
|
recover() {
|
|
1744
|
-
if (
|
|
1898
|
+
if (existsSync2(this.mapPath)) {
|
|
1745
1899
|
try {
|
|
1746
1900
|
const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
|
|
1747
1901
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
1748
1902
|
} catch {
|
|
1749
1903
|
}
|
|
1750
1904
|
}
|
|
1751
|
-
if (
|
|
1905
|
+
if (existsSync2(this.outboxPath)) {
|
|
1752
1906
|
const txt = readFileSync3(this.outboxPath, "utf8");
|
|
1753
1907
|
for (const line of txt.split("\n")) {
|
|
1754
1908
|
const s = line.trim();
|
|
@@ -1769,7 +1923,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1769
1923
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
1770
1924
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
1771
1925
|
replayGlobalEvents() {
|
|
1772
|
-
if (!
|
|
1926
|
+
if (!existsSync2(this.stormPath)) return;
|
|
1773
1927
|
let txt;
|
|
1774
1928
|
try {
|
|
1775
1929
|
txt = readFileSync3(this.stormPath, "utf8");
|
|
@@ -2440,7 +2594,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
2440
2594
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
2441
2595
|
*/
|
|
2442
2596
|
reconcileRuntime(opts) {
|
|
2443
|
-
const wtExists = opts.worktreeExists ??
|
|
2597
|
+
const wtExists = opts.worktreeExists ?? existsSync2;
|
|
2444
2598
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
2445
2599
|
try {
|
|
2446
2600
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -3237,7 +3391,23 @@ var SwarmClient = class _SwarmClient {
|
|
|
3237
3391
|
} catch {
|
|
3238
3392
|
break;
|
|
3239
3393
|
}
|
|
3240
|
-
if (res?.isError)
|
|
3394
|
+
if (res?.isError) {
|
|
3395
|
+
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";
|
|
3396
|
+
const tries = (this.rejected.get(e.seq) ?? 0) + 1;
|
|
3397
|
+
this.rejected.set(e.seq, tries);
|
|
3398
|
+
if (tries >= _SwarmClient.REJECT_LIMIT) {
|
|
3399
|
+
this.pending.shift();
|
|
3400
|
+
this.rejected.delete(e.seq);
|
|
3401
|
+
console.error(
|
|
3402
|
+
`[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}`
|
|
3403
|
+
);
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3406
|
+
console.error(
|
|
3407
|
+
`[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.`
|
|
3408
|
+
);
|
|
3409
|
+
break;
|
|
3410
|
+
}
|
|
3241
3411
|
if (e.t === "run") {
|
|
3242
3412
|
const run = res.structuredContent?.run;
|
|
3243
3413
|
if (run?.id && this.prodIdByKey.get(e.runKey) !== run.id) {
|
|
@@ -3297,6 +3467,9 @@ function parseDecision(v) {
|
|
|
3297
3467
|
}
|
|
3298
3468
|
}
|
|
3299
3469
|
|
|
3470
|
+
// src/cli.ts
|
|
3471
|
+
init_config();
|
|
3472
|
+
|
|
3300
3473
|
// src/daemonLaunch.ts
|
|
3301
3474
|
import { openSync as openSync2, closeSync as closeSync2, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
|
|
3302
3475
|
import { spawn } from "child_process";
|
|
@@ -3608,6 +3781,7 @@ async function cleanupWorktreeProcesses(worktree, opts = {}) {
|
|
|
3608
3781
|
}
|
|
3609
3782
|
|
|
3610
3783
|
// src/repoRegistry.ts
|
|
3784
|
+
init_config();
|
|
3611
3785
|
import { $ } from "execa";
|
|
3612
3786
|
function parseRepoFullName(remoteUrl) {
|
|
3613
3787
|
const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
|
|
@@ -3633,14 +3807,14 @@ async function buildRepoRegistry(paths) {
|
|
|
3633
3807
|
|
|
3634
3808
|
// src/hostIdentity.ts
|
|
3635
3809
|
import { hostname } from "os";
|
|
3636
|
-
import { existsSync as
|
|
3810
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
3637
3811
|
import { join as join3 } from "path";
|
|
3638
3812
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3639
3813
|
function hostIdentityPath(home) {
|
|
3640
3814
|
return join3(home, "host.json");
|
|
3641
3815
|
}
|
|
3642
3816
|
function loadHostIdentity(deps) {
|
|
3643
|
-
const exists = deps.exists ??
|
|
3817
|
+
const exists = deps.exists ?? existsSync3;
|
|
3644
3818
|
const readFile2 = deps.readFile ?? ((p) => readFileSync5(p, "utf8"));
|
|
3645
3819
|
const writeFile2 = deps.writeFile ?? ((p, data) => {
|
|
3646
3820
|
writeFileSync4(`${p}.tmp`, data);
|
|
@@ -3670,6 +3844,7 @@ function loadHostIdentity(deps) {
|
|
|
3670
3844
|
}
|
|
3671
3845
|
|
|
3672
3846
|
// src/runList.ts
|
|
3847
|
+
init_src();
|
|
3673
3848
|
function hostGroupHeader(g, thisHostId) {
|
|
3674
3849
|
const running = g.runs.filter((r) => r.status === "running").length;
|
|
3675
3850
|
const mine = !g.unknown && g.key === thisHostId ? " (\u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430)" : "";
|
|
@@ -3701,6 +3876,7 @@ function renderRunList(runs, opts) {
|
|
|
3701
3876
|
}
|
|
3702
3877
|
|
|
3703
3878
|
// src/hostPick.ts
|
|
3879
|
+
init_src();
|
|
3704
3880
|
function chooseSpawnHost(flag) {
|
|
3705
3881
|
if (flag == null) return { kind: "self" };
|
|
3706
3882
|
const ref = normalizeHostRef(flag);
|
|
@@ -3747,6 +3923,7 @@ function hostLines(hosts, opts = {}) {
|
|
|
3747
3923
|
}
|
|
3748
3924
|
|
|
3749
3925
|
// src/web.ts
|
|
3926
|
+
init_src();
|
|
3750
3927
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
3751
3928
|
import { basename as basename2 } from "path";
|
|
3752
3929
|
import { serve as serve2 } from "@hono/node-server";
|
|
@@ -3940,7 +4117,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
3940
4117
|
// package.json
|
|
3941
4118
|
var package_default = {
|
|
3942
4119
|
name: "@taskless-app/swarm",
|
|
3943
|
-
version: "0.4.
|
|
4120
|
+
version: "0.4.9",
|
|
3944
4121
|
type: "module",
|
|
3945
4122
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
3946
4123
|
keywords: [
|
|
@@ -3989,13 +4166,14 @@ var package_default = {
|
|
|
3989
4166
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
3990
4167
|
execa: "^10.0.0",
|
|
3991
4168
|
zod: "3.25.76"
|
|
3992
|
-
},
|
|
3993
|
-
optionalDependencies: {
|
|
3994
|
-
"better-sqlite3": "^12.11.1"
|
|
3995
4169
|
}
|
|
3996
4170
|
};
|
|
3997
4171
|
|
|
4172
|
+
// src/web.ts
|
|
4173
|
+
init_storm();
|
|
4174
|
+
|
|
3998
4175
|
// src/gh.ts
|
|
4176
|
+
init_src();
|
|
3999
4177
|
import { $ as $3 } from "execa";
|
|
4000
4178
|
var NO_CHECKS_STDERR = /no checks reported/i;
|
|
4001
4179
|
var realChecksRunner = async (pr) => {
|
|
@@ -4041,7 +4219,11 @@ async function ghChecks(pr, run = realChecksRunner) {
|
|
|
4041
4219
|
return { bucket, text, tone, total, pass, fail, pending: pending2 };
|
|
4042
4220
|
}
|
|
4043
4221
|
|
|
4222
|
+
// src/web.ts
|
|
4223
|
+
init_config();
|
|
4224
|
+
|
|
4044
4225
|
// src/droneChannel.ts
|
|
4226
|
+
init_src();
|
|
4045
4227
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
4046
4228
|
import { chmodSync, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
4047
4229
|
import { dirname as dirname2 } from "path";
|
|
@@ -4087,6 +4269,7 @@ function verifyLocalCliToken(token, secret) {
|
|
|
4087
4269
|
var EVENT_KIND_SET = new Set(EVENT_KINDS);
|
|
4088
4270
|
|
|
4089
4271
|
// src/oauth.ts
|
|
4272
|
+
init_config();
|
|
4090
4273
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
4091
4274
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync } from "fs";
|
|
4092
4275
|
import { join as join5 } from "path";
|
|
@@ -5785,14 +5968,14 @@ import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDronePro
|
|
|
5785
5968
|
|
|
5786
5969
|
// src/launcher.ts
|
|
5787
5970
|
import { spawn as spawn2 } from "child_process";
|
|
5788
|
-
import { closeSync as closeSync3, existsSync as
|
|
5789
|
-
import { createRequire
|
|
5971
|
+
import { closeSync as closeSync3, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync as openSync3, readdirSync as readdirSync4, statSync as statSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
5972
|
+
import { createRequire } from "module";
|
|
5790
5973
|
import { homedir as homedir2 } from "os";
|
|
5791
5974
|
import { dirname as dirname3, join as join7 } from "path";
|
|
5792
5975
|
import { fileURLToPath } from "url";
|
|
5793
5976
|
|
|
5794
5977
|
// src/claudeBin.ts
|
|
5795
|
-
import { existsSync as
|
|
5978
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
5796
5979
|
import { win32 as winPath } from "path";
|
|
5797
5980
|
var CLAUDE_FALLBACK = "claude";
|
|
5798
5981
|
function compareVersionDirs(a, b) {
|
|
@@ -5829,7 +6012,7 @@ function winVersionRoots(env, listDir) {
|
|
|
5829
6012
|
function findWindowsClaude(opts = {}) {
|
|
5830
6013
|
const env = opts.env ?? process.env;
|
|
5831
6014
|
const listDir = opts.listDir ?? realListDir;
|
|
5832
|
-
const fileExists = opts.fileExists ??
|
|
6015
|
+
const fileExists = opts.fileExists ?? existsSync4;
|
|
5833
6016
|
let best = null;
|
|
5834
6017
|
for (const root of winVersionRoots(env, listDir)) {
|
|
5835
6018
|
for (const version of listDir(root)) {
|
|
@@ -5983,7 +6166,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
|
|
|
5983
6166
|
}
|
|
5984
6167
|
|
|
5985
6168
|
// src/launcher.ts
|
|
5986
|
-
var requireFromHere =
|
|
6169
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
5987
6170
|
function resolveTsxCli() {
|
|
5988
6171
|
try {
|
|
5989
6172
|
return requireFromHere.resolve("tsx/cli");
|
|
@@ -6049,7 +6232,7 @@ function roleDir(worktree, kind) {
|
|
|
6049
6232
|
return join7(worktree, ".claude", "roles", kind);
|
|
6050
6233
|
}
|
|
6051
6234
|
function roleDirs(worktree, kind) {
|
|
6052
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
6235
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync5(dir));
|
|
6053
6236
|
}
|
|
6054
6237
|
function nextRunLogPath(runLogsDir, runId) {
|
|
6055
6238
|
const dir = join7(runLogsDir, String(runId));
|
|
@@ -6145,7 +6328,7 @@ function makeLauncher(cfg) {
|
|
|
6145
6328
|
const realHome = cfg.droneIsolation?.realHome ?? homedir2();
|
|
6146
6329
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
6147
6330
|
const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
|
|
6148
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !
|
|
6331
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync5(join7(cfg.droneIsolation.swarmHome, "token"))) {
|
|
6149
6332
|
console.error(
|
|
6150
6333
|
`[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.`
|
|
6151
6334
|
);
|
|
@@ -6178,7 +6361,7 @@ function makeLauncher(cfg) {
|
|
|
6178
6361
|
}
|
|
6179
6362
|
|
|
6180
6363
|
// src/git.ts
|
|
6181
|
-
import { existsSync as
|
|
6364
|
+
import { existsSync as existsSync6, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
6182
6365
|
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
6183
6366
|
import { isAbsolute, join as join8 } from "path";
|
|
6184
6367
|
import { $ as $4 } from "execa";
|
|
@@ -6196,7 +6379,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
6196
6379
|
}
|
|
6197
6380
|
}
|
|
6198
6381
|
async function ensureWorktreeDirGone(worktree) {
|
|
6199
|
-
if (!
|
|
6382
|
+
if (!existsSync6(worktree)) return false;
|
|
6200
6383
|
if (!isLinkedWorktree(worktree)) {
|
|
6201
6384
|
console.error(
|
|
6202
6385
|
`[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.`
|
|
@@ -6211,7 +6394,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
6211
6394
|
);
|
|
6212
6395
|
return true;
|
|
6213
6396
|
}
|
|
6214
|
-
if (
|
|
6397
|
+
if (existsSync6(worktree)) {
|
|
6215
6398
|
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.`);
|
|
6216
6399
|
}
|
|
6217
6400
|
return true;
|
|
@@ -6224,6 +6407,7 @@ async function removeWorktree(repo, path) {
|
|
|
6224
6407
|
}
|
|
6225
6408
|
|
|
6226
6409
|
// src/agentToken.ts
|
|
6410
|
+
init_config();
|
|
6227
6411
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
6228
6412
|
import { join as join9 } from "path";
|
|
6229
6413
|
var STORE_PATH2 = join9(SWARM_HOME, "agent.json");
|
|
@@ -6270,6 +6454,7 @@ async function probeAgentToken(args, fetchFn = fetch) {
|
|
|
6270
6454
|
}
|
|
6271
6455
|
|
|
6272
6456
|
// src/taskless.ts
|
|
6457
|
+
init_src();
|
|
6273
6458
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6274
6459
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6275
6460
|
|
|
@@ -6288,7 +6473,11 @@ import { readFile as fsReadFile } from "fs/promises";
|
|
|
6288
6473
|
import { basename as basename3, isAbsolute as isAbsolute2, join as join11 } from "path";
|
|
6289
6474
|
var SHOT_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
6290
6475
|
|
|
6476
|
+
// src/taskless.ts
|
|
6477
|
+
init_config();
|
|
6478
|
+
|
|
6291
6479
|
// src/workspace.ts
|
|
6480
|
+
init_config();
|
|
6292
6481
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3 } from "fs";
|
|
6293
6482
|
import { join as join12 } from "path";
|
|
6294
6483
|
var WORKSPACE_PATH = join12(SWARM_HOME, "workspace.json");
|
|
@@ -6643,6 +6832,7 @@ async function launchChromium(opts = {}) {
|
|
|
6643
6832
|
}
|
|
6644
6833
|
|
|
6645
6834
|
// src/shotCli.ts
|
|
6835
|
+
init_config();
|
|
6646
6836
|
function isFlagValue(v) {
|
|
6647
6837
|
return v !== void 0 && !v.startsWith("--");
|
|
6648
6838
|
}
|
|
@@ -6717,6 +6907,7 @@ async function runShotCli(argv, deps = {}) {
|
|
|
6717
6907
|
}
|
|
6718
6908
|
|
|
6719
6909
|
// src/daemonLink.ts
|
|
6910
|
+
init_config();
|
|
6720
6911
|
var TIMEOUT_MS = 5e3;
|
|
6721
6912
|
var ACTION_TIMEOUT_MS = 2e4;
|
|
6722
6913
|
var BASE = `http://127.0.0.1:${WEB_PORT}`;
|
|
@@ -6782,14 +6973,14 @@ async function daemonStatus2() {
|
|
|
6782
6973
|
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`;
|
|
6783
6974
|
|
|
6784
6975
|
// src/pauseState.ts
|
|
6785
|
-
import { existsSync as
|
|
6976
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
6786
6977
|
import { join as join13 } from "path";
|
|
6787
6978
|
function pauseStatePath(home) {
|
|
6788
6979
|
return join13(home, "pause.json");
|
|
6789
6980
|
}
|
|
6790
6981
|
var ACTIVE = { paused: false, at: "", source: "cli" };
|
|
6791
6982
|
function loadPauseState(home, io = {}) {
|
|
6792
|
-
const exists = io.exists ??
|
|
6983
|
+
const exists = io.exists ?? existsSync7;
|
|
6793
6984
|
const readFile2 = io.readFile ?? ((p) => readFileSync10(p, "utf8"));
|
|
6794
6985
|
const path = pauseStatePath(home);
|
|
6795
6986
|
if (!exists(path)) return ACTIVE;
|
|
@@ -6809,7 +7000,7 @@ function loadPauseState(home, io = {}) {
|
|
|
6809
7000
|
function savePauseState(home, state, io = {}) {
|
|
6810
7001
|
const path = pauseStatePath(home);
|
|
6811
7002
|
if (!state.paused) {
|
|
6812
|
-
const remove = io.remove ?? ((p) =>
|
|
7003
|
+
const remove = io.remove ?? ((p) => existsSync7(p) && unlinkSync4(p));
|
|
6813
7004
|
remove(path);
|
|
6814
7005
|
return;
|
|
6815
7006
|
}
|
|
@@ -6855,6 +7046,7 @@ function readHeartbeat(path) {
|
|
|
6855
7046
|
}
|
|
6856
7047
|
|
|
6857
7048
|
// src/cli.ts
|
|
7049
|
+
init_src();
|
|
6858
7050
|
var PACKAGE_VERSION2 = package_default.version;
|
|
6859
7051
|
function takeRepo(argv) {
|
|
6860
7052
|
const i = argv.indexOf("--repo");
|
|
@@ -7277,7 +7469,9 @@ function usage() {
|
|
|
7277
7469
|
swarm login [--workspace <slug|id>] [--no-open]
|
|
7278
7470
|
\u0432\u043E\u0439\u0442\u0438 \u0432 Taskless \u0438 \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441
|
|
7279
7471
|
swarm start [--repo <path>] [--foreground]
|
|
7280
|
-
\u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0440\u043E\u0439 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u043E\u0447\u0435\u0440\u0435\u0434\u044C, \u0441\u043F\u0430\u0432\u043D\u0438\u0442 \u0438 \u0431\u0443\u0434\u0438\u0442 \u0434\u0440\u043E\u043D\u043E\u0432
|
|
7472
|
+
\u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0440\u043E\u0439 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u043E\u0447\u0435\u0440\u0435\u0434\u044C, \u0441\u043F\u0430\u0432\u043D\u0438\u0442 \u0438 \u0431\u0443\u0434\u0438\u0442 \u0434\u0440\u043E\u043D\u043E\u0432.
|
|
7473
|
+
\u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0430\u0445\u043E\u0434\u0438\u0442 \u0441\u0430\u043C \u2014 \u043A\u043B\u043E\u043D\u044B \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 Taskless \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435;
|
|
7474
|
+
--repo \u043D\u0443\u0436\u0435\u043D, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043E\u043D\u0438 \u043B\u0435\u0436\u0430\u0442 \u0432 \u043D\u0435\u043E\u0431\u044B\u0447\u043D\u043E\u043C \u043C\u0435\u0441\u0442\u0435
|
|
7281
7475
|
swarm status \u0436\u0438\u0432 \u043B\u0438 \u0434\u0435\u043C\u043E\u043D, \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u043B\u0438 \u0440\u043E\u0439, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0434\u0440\u043E\u043D\u043E\u0432 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435
|
|
7282
7476
|
|
|
7283
7477
|
\u0414\u0410\u0422\u042C \u0420\u041E\u042E \u0420\u0410\u0411\u041E\u0422\u0423
|
|
@@ -7350,7 +7544,10 @@ function effectiveWorktreesDir() {
|
|
|
7350
7544
|
return effectiveWorktreesDirMemo;
|
|
7351
7545
|
}
|
|
7352
7546
|
async function makeCliStore(repo) {
|
|
7353
|
-
if (SWARM_BACKEND !== "client")
|
|
7547
|
+
if (SWARM_BACKEND !== "client") {
|
|
7548
|
+
const { SwarmDb: SwarmDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
7549
|
+
return new SwarmDb2(DB_PATH, repo);
|
|
7550
|
+
}
|
|
7354
7551
|
const call = resilientSwarmMcp({
|
|
7355
7552
|
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.`),
|
|
7356
7553
|
onUp: () => console.warn("Swarm MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
|
|
@@ -7408,7 +7605,8 @@ if (cmd === "import-db") {
|
|
|
7408
7605
|
console.error("\u043D\u0443\u0436\u043D\u043E: swarm import-db <path> --repo <path>");
|
|
7409
7606
|
process.exit(1);
|
|
7410
7607
|
}
|
|
7411
|
-
const
|
|
7608
|
+
const { SwarmDb: SwarmDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
7609
|
+
const sdb = new SwarmDb2(DB_PATH, repo);
|
|
7412
7610
|
const res = sdb.importDb(path, repo);
|
|
7413
7611
|
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})`);
|
|
7414
7612
|
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`);
|
|
@@ -7422,14 +7620,11 @@ if (cmd === "start") {
|
|
|
7422
7620
|
}
|
|
7423
7621
|
const { repo: repoFlag } = takeRepo(startArgs);
|
|
7424
7622
|
const repo = repoFlag ?? SWARM_REPO;
|
|
7425
|
-
if (!repo) {
|
|
7426
|
-
console.warn("SWARM_REPO \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0435\u0437 \u044F\u0432\u043D\u043E\u0433\u043E repo \u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u044F\u0442\u0441\u044F (\u043D\u0443\u0436\u0435\u043D spawn --repo).");
|
|
7427
|
-
}
|
|
7428
7623
|
if (!foreground) {
|
|
7429
7624
|
process.exit(await runStartDetached(startArgs));
|
|
7430
7625
|
}
|
|
7431
7626
|
try {
|
|
7432
|
-
await runDaemon({ repo });
|
|
7627
|
+
await runDaemon({ repo, repoFlag });
|
|
7433
7628
|
} catch (e) {
|
|
7434
7629
|
console.error(`\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F: ${e instanceof Error ? e.message : e}`);
|
|
7435
7630
|
process.exit(1);
|