@taskless-app/swarm 0.1.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/overmind.js CHANGED
@@ -8,20 +8,400 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // ../swarm-core/src/types.ts
12
+ function isMilestoneKind(kind) {
13
+ return MILESTONE_KIND_SET.has(kind);
14
+ }
15
+ var ACTIVE_DRONE_STATUSES, MILESTONE_KINDS, MILESTONE_KIND_SET;
16
+ var init_types = __esm({
17
+ "../swarm-core/src/types.ts"() {
18
+ "use strict";
19
+ ACTIVE_DRONE_STATUSES = /* @__PURE__ */ new Set(["running", "blocked"]);
20
+ MILESTONE_KINDS = [
21
+ "spawned",
22
+ "pr_opened",
23
+ "merged",
24
+ "done",
25
+ "failed",
26
+ "stalled",
27
+ "conflict",
28
+ "ci_wake",
29
+ // CI-побудка (TASWM-102) — тоже реконсайл-решение, веха как conflict
30
+ "stopped",
31
+ "superseded",
32
+ "paused",
33
+ "resumed",
34
+ "storm_start",
35
+ "storm_end",
36
+ "storm_dead",
37
+ "anomaly"
38
+ // надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
39
+ ];
40
+ MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
41
+ }
42
+ });
43
+
44
+ // ../swarm-core/src/timeline.ts
45
+ function buildSegments(created, events, laneEnd) {
46
+ const segs = [];
47
+ let cursor = created;
48
+ let phase = "queued";
49
+ const push = (to) => {
50
+ if (to > cursor) segs.push({ phase, from: cursor, to });
51
+ cursor = to;
52
+ };
53
+ for (const e of events) {
54
+ switch (e.kind) {
55
+ case "step":
56
+ if (phase === "queued") {
57
+ push(e.at);
58
+ phase = "running";
59
+ }
60
+ break;
61
+ case "blocked":
62
+ push(e.at);
63
+ phase = "blocked";
64
+ break;
65
+ case "answered":
66
+ push(e.at);
67
+ phase = "running";
68
+ break;
69
+ case "pr_opened":
70
+ push(e.at);
71
+ phase = "review";
72
+ break;
73
+ case "merged":
74
+ case "done":
75
+ push(e.at);
76
+ phase = "done";
77
+ break;
78
+ case "failed":
79
+ push(e.at);
80
+ phase = "failed";
81
+ break;
82
+ case "stalled":
83
+ push(e.at);
84
+ phase = "stalled";
85
+ break;
86
+ case "paused":
87
+ case "storm_dead":
88
+ push(e.at);
89
+ phase = "paused";
90
+ break;
91
+ case "resumed":
92
+ push(e.at);
93
+ phase = "running";
94
+ break;
95
+ // TASWM-166: раньше эти два kind'а не двигали фазу — хвост дорожки
96
+ // дорисовывался ТЕКУЩЕЙ (до остановки) фазой, напр. "running" сплошным
97
+ // зелёным до самого конца — визуально неотличимо от реально идущего прогона.
98
+ case "stopped":
99
+ push(e.at);
100
+ phase = "stopped";
101
+ break;
102
+ case "superseded":
103
+ push(e.at);
104
+ phase = "superseded";
105
+ break;
106
+ }
107
+ }
108
+ push(laneEnd);
109
+ return segs;
110
+ }
111
+ function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
112
+ switch (status) {
113
+ case "queued": {
114
+ const unmet = deps.filter((d) => !d.met);
115
+ if (unmet.length) {
116
+ const who = unmet.map((d) => d.name ? `${d.slug}\xB7${d.name}` : d.slug).join(", ");
117
+ return { icon: "\u26D4", text: `\u0436\u0434\u0451\u0442 ${who}`, tone: "dep" };
118
+ }
119
+ return { icon: "\u23F8", text: "\u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430 / \u0441\u043F\u0430\u0432\u043D\u0430", tone: "queued" };
120
+ }
121
+ case "running":
122
+ return { icon: "\u25B6", text: lastStep ? `\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442: ${lastStep}` : "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", tone: "run" };
123
+ case "blocked":
124
+ return { icon: "\u2753", text: openQ ? `\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430: ${openQ}` : "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430", tone: "blocked" };
125
+ case "paused":
126
+ return { icon: "\u{1F4A4}", text: stopReason || "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "paused" };
127
+ case "storm_dead":
128
+ return { icon: "\u{1F4A4}", text: "\u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u0448\u0442\u043E\u0440\u043C Anthropic, \u0436\u0434\u0451\u0442 \u043E\u0442\u0431\u043E\u044F", tone: "paused" };
129
+ case "review": {
130
+ if (!pr) return { icon: "\u{1F440}", text: "review \u0431\u0435\u0437 PR", tone: "review" };
131
+ if (!ci) return { icon: "\u23F3", text: "CI: \u0436\u0434\u0443 \u0441\u0442\u0430\u0442\u0443\u0441\u2026", tone: "review" };
132
+ if (ci.tone === "merge") return { icon: "\u{1F535}", text: "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430", tone: "merge" };
133
+ return { icon: ci.tone === "fail" ? "\u274C" : "\u23F3", text: ci.text.replace(/^[^ ]+ /, ""), tone: ci.tone };
134
+ }
135
+ case "stalled":
136
+ return { icon: "\u26A0\uFE0F", text: stopReason || (lastStep ? `\u043C\u043E\u043B\u0447\u0438\u0442: ${lastStep}` : "\u043C\u043E\u043B\u0447\u0438\u0442"), tone: "stalled" };
137
+ case "failed":
138
+ return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
139
+ case "stopped":
140
+ return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "stopped" };
141
+ case "superseded":
142
+ return { icon: "\u21BA", text: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442", tone: "superseded" };
143
+ case "done":
144
+ return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
145
+ default:
146
+ return { icon: "\u2022", text: status, tone: "queued" };
147
+ }
148
+ }
149
+ function pickHeadline(lanes) {
150
+ const live = lanes.filter((l2) => l2.live && !l2.obsolete);
151
+ const by = (pred) => live.find(pred);
152
+ const fmt = (l2, extra) => `${l2.name} \xB7 ${l2.slug} \u2014 ${extra}`;
153
+ let l = by((x) => x.status === "review" && x.ci?.tone === "fail");
154
+ if (l) return { icon: "\u274C", text: fmt(l, "CI \u0443\u043F\u0430\u043B \u2014 \u0447\u0438\u043D\u0438\u0442\u044C"), tone: "fail" };
155
+ l = by((x) => x.status === "blocked");
156
+ if (l) return { icon: "\u2753", text: fmt(l, "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430"), tone: "blocked" };
157
+ l = by((x) => x.status === "stalled");
158
+ if (l) return { icon: "\u26A0\uFE0F", text: fmt(l, "stalled \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430"), tone: "stalled" };
159
+ l = by((x) => x.status === "review" && x.blocker.tone === "merge");
160
+ if (l) return { icon: "\u{1F535}", text: fmt(l, "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430"), tone: "merge" };
161
+ l = by((x) => x.status === "queued" && x.dependsOn.some((d) => !d.met));
162
+ if (l) {
163
+ const dep = l.dependsOn.find((d) => !d.met);
164
+ return { icon: "\u26D4", text: fmt(l, `\u0436\u0434\u0451\u0442 ${dep.slug}${dep.name ? " \xB7 " + dep.name : ""}`), tone: "dep" };
165
+ }
166
+ l = by((x) => x.status === "review");
167
+ if (l) return { icon: "\u23F3", text: fmt(l, "review \u2014 CI \u0438\u0434\u0451\u0442"), tone: "review" };
168
+ l = by((x) => x.status === "running");
169
+ if (l) return { icon: "\u25B6", text: `${live.filter((x) => x.status === "running").length} \u0434\u0440\u043E\u043D\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435 \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043E\u043A \u043D\u0435\u0442`, tone: "run" };
170
+ return null;
171
+ }
172
+ var DEFAULT_RUN_MS;
173
+ var init_timeline = __esm({
174
+ "../swarm-core/src/timeline.ts"() {
175
+ "use strict";
176
+ DEFAULT_RUN_MS = 25 * 6e4;
177
+ }
178
+ });
179
+
180
+ // ../swarm-core/src/status.ts
181
+ function statusMeta(status) {
182
+ return STATUS_META[status] ?? FALLBACK;
183
+ }
184
+ function isTerminalStatus(status) {
185
+ return statusMeta(status).terminal;
186
+ }
187
+ function statusTuples() {
188
+ const out = {};
189
+ for (const [status, m] of Object.entries(STATUS_META)) {
190
+ out[status] = [m.pill, m.label, m.color];
191
+ }
192
+ return out;
193
+ }
194
+ var STATUS_META, FALLBACK, TERMINAL_STATUSES;
195
+ var init_status = __esm({
196
+ "../swarm-core/src/status.ts"() {
197
+ "use strict";
198
+ STATUS_META = {
199
+ queued: { pill: "queued", label: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438", color: "--muted-foreground", terminal: false },
200
+ running: { pill: "run", label: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", color: "--primary", terminal: false },
201
+ blocked: { pill: "blocked", label: "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430", color: "--warning", terminal: false },
202
+ paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning", terminal: false },
203
+ // overmind был недоступен — ждёт пробуждения (TASWM-41)
204
+ review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info", terminal: false },
205
+ done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success", terminal: true },
206
+ failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive", terminal: true },
207
+ stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive", terminal: false },
208
+ // молчит/погиб, но ещё разруливается — не терминал
209
+ storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning", terminal: false },
210
+ // умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
211
+ stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground", terminal: true },
212
+ superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true }
213
+ };
214
+ FALLBACK = STATUS_META.queued;
215
+ TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
216
+ (s) => STATUS_META[s].terminal
217
+ );
218
+ }
219
+ });
220
+
221
+ // ../swarm-core/src/time.ts
222
+ function stamp(d = /* @__PURE__ */ new Date()) {
223
+ return d.toISOString().slice(0, 19).replace("T", " ");
224
+ }
225
+ function stampFromEpochSec(sec) {
226
+ return stamp(new Date(sec * 1e3));
227
+ }
228
+ function sqliteMs(ts) {
229
+ return Date.parse(ts.replace(" ", "T") + "Z");
230
+ }
231
+ function toStamp(v) {
232
+ if (typeof v !== "string" || !v) return null;
233
+ const d = new Date(v);
234
+ if (Number.isNaN(d.getTime())) return v.includes("T") ? v.slice(0, 19).replace("T", " ") : v;
235
+ return stamp(d);
236
+ }
237
+ var DAY_MS;
238
+ var init_time = __esm({
239
+ "../swarm-core/src/time.ts"() {
240
+ "use strict";
241
+ DAY_MS = 864e5;
242
+ }
243
+ });
244
+
245
+ // ../swarm-core/src/windows.ts
246
+ function resolveRoyWindows(env = {}) {
247
+ return {
248
+ windowMs: Number(env.ROY_WINDOW_H || ROY_WINDOW_DEFAULTS.windowH) * 36e5,
249
+ maxTerminalLanes: Number(env.ROY_MAX_DONE || ROY_WINDOW_DEFAULTS.maxDone),
250
+ maxAxisMs: Number(env.ROY_AXIS_H || ROY_WINDOW_DEFAULTS.axisH) * 36e5
251
+ };
252
+ }
253
+ var ROY_WINDOW_DEFAULTS;
254
+ var init_windows = __esm({
255
+ "../swarm-core/src/windows.ts"() {
256
+ "use strict";
257
+ ROY_WINDOW_DEFAULTS = { windowH: 5, maxDone: 6, axisH: 6 };
258
+ }
259
+ });
260
+
261
+ // ../swarm-core/src/slug.ts
262
+ var init_slug = __esm({
263
+ "../swarm-core/src/slug.ts"() {
264
+ "use strict";
265
+ }
266
+ });
267
+
268
+ // ../swarm-core/src/text.ts
269
+ function extractText(d) {
270
+ if (typeof d === "string") return d;
271
+ const out = [];
272
+ const walk = (n) => {
273
+ if (!n || typeof n !== "object") return;
274
+ const node = n;
275
+ if (node.text) out.push(node.text);
276
+ (node.content ?? []).forEach(walk);
277
+ };
278
+ walk(d);
279
+ return out.join(" ");
280
+ }
281
+ var init_text = __esm({
282
+ "../swarm-core/src/text.ts"() {
283
+ "use strict";
284
+ }
285
+ });
286
+
287
+ // ../swarm-core/src/names.ts
288
+ function toRoman(n) {
289
+ if (n < 1 || n > 3999) return String(n);
290
+ const table = [
291
+ [1e3, "M"],
292
+ [900, "CM"],
293
+ [500, "D"],
294
+ [400, "CD"],
295
+ [100, "C"],
296
+ [90, "XC"],
297
+ [50, "L"],
298
+ [40, "XL"],
299
+ [10, "X"],
300
+ [9, "IX"],
301
+ [5, "V"],
302
+ [4, "IV"],
303
+ [1, "I"]
304
+ ];
305
+ let out = "";
306
+ for (const [v, sym] of table) while (n >= v) out += sym, n -= v;
307
+ return out;
308
+ }
309
+ function genName(base2, g) {
310
+ return g <= 1 ? base2 : `${base2} ${toRoman(g)}`;
311
+ }
312
+ function baseName(name) {
313
+ return name.split(" ")[0];
314
+ }
315
+ var DRONE_POOL, ACTIVE_NAME_STATUSES;
316
+ var init_names = __esm({
317
+ "../swarm-core/src/names.ts"() {
318
+ "use strict";
319
+ DRONE_POOL = [
320
+ // Латинский бестиарий (15)
321
+ "SCARAB",
322
+ "VESPA",
323
+ "MANTIS",
324
+ "HORNET",
325
+ "CICADA",
326
+ "LOCUST",
327
+ "ATTA",
328
+ "FORMICA",
329
+ "MYRMEX",
330
+ "APIS",
331
+ "TERMES",
332
+ "GRYLLUS",
333
+ "LAMPYRIS",
334
+ "ARACHNE",
335
+ "SPHINX",
336
+ // Мифо-стаи (9)
337
+ "TIAMAT",
338
+ "JORMUNGAND",
339
+ "GRENDEL",
340
+ "TYPHON",
341
+ "FENRIS",
342
+ "GARM",
343
+ "KUKULKAN",
344
+ "LEVIATHAN",
345
+ "SURTUR"
346
+ ];
347
+ ACTIVE_NAME_STATUSES = ["queued", "running", "blocked", "review"];
348
+ }
349
+ });
350
+
351
+ // ../swarm-core/src/index.ts
352
+ var init_src = __esm({
353
+ "../swarm-core/src/index.ts"() {
354
+ "use strict";
355
+ init_types();
356
+ init_timeline();
357
+ init_status();
358
+ init_time();
359
+ init_windows();
360
+ init_slug();
361
+ init_text();
362
+ init_names();
363
+ }
364
+ });
365
+
11
366
  // src/config.ts
12
367
  var config_exports = {};
13
368
  __export(config_exports, {
14
369
  DB_PATH: () => DB_PATH,
15
370
  HEARTBEAT_PATH: () => HEARTBEAT_PATH,
16
371
  HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
372
+ REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
373
+ RUN_LOGS_DIR: () => RUN_LOGS_DIR,
374
+ SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
375
+ SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
376
+ SWARM_ANOMALY_OUTBOX_STUCK_MS: () => SWARM_ANOMALY_OUTBOX_STUCK_MS,
377
+ SWARM_ANOMALY_PR_NO_REVIEWER_MS: () => SWARM_ANOMALY_PR_NO_REVIEWER_MS,
378
+ SWARM_ANOMALY_PR_STUCK_MERGE_MS: () => SWARM_ANOMALY_PR_STUCK_MERGE_MS,
379
+ SWARM_ANOMALY_QUESTION_UNANSWERED_MS: () => SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
380
+ SWARM_ANOMALY_QUEUE_STALLED_MS: () => SWARM_ANOMALY_QUEUE_STALLED_MS,
381
+ SWARM_ANOMALY_RENOTIFY_MS: () => SWARM_ANOMALY_RENOTIFY_MS,
382
+ SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
17
383
  SWARM_BACKEND: () => SWARM_BACKEND,
18
384
  SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
19
385
  SWARM_HOME: () => SWARM_HOME,
386
+ SWARM_MAX_FD_LOAD_FACTOR: () => SWARM_MAX_FD_LOAD_FACTOR,
20
387
  SWARM_MAX_IN_FLIGHT: () => SWARM_MAX_IN_FLIGHT,
388
+ SWARM_MAX_LOAD_FACTOR: () => SWARM_MAX_LOAD_FACTOR,
389
+ SWARM_MIN_FREE_GB: () => SWARM_MIN_FREE_GB,
21
390
  SWARM_OUTBOX: () => SWARM_OUTBOX,
391
+ SWARM_PROD_HEARTBEAT_MS: () => SWARM_PROD_HEARTBEAT_MS,
392
+ SWARM_REHYDRATE_MS: () => SWARM_REHYDRATE_MS,
393
+ SWARM_RELIEF_LOAD_FACTOR: () => SWARM_RELIEF_LOAD_FACTOR,
394
+ SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
395
+ SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
22
396
  SWARM_REPO: () => SWARM_REPO,
397
+ SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
398
+ SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
399
+ SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
23
400
  SWARM_STORM_URL: () => SWARM_STORM_URL,
401
+ SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
402
+ SWARM_VERIFY: () => SWARM_VERIFY,
24
403
  TASKLESS_API: () => TASKLESS_API,
404
+ TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
25
405
  TASKLESS_TASK_URL_BASE: () => TASKLESS_TASK_URL_BASE,
26
406
  TASKLESS_TOKEN: () => TASKLESS_TOKEN,
27
407
  TASKLESS_WORKSPACE: () => TASKLESS_WORKSPACE,
@@ -29,11 +409,14 @@ __export(config_exports, {
29
409
  WEB_PORT: () => WEB_PORT,
30
410
  WEB_REDIRECT_URI: () => WEB_REDIRECT_URI,
31
411
  WORKTREES_DIR: () => WORKTREES_DIR,
32
- normalizeRepo: () => normalizeRepo
412
+ droneWorktreePath: () => droneWorktreePath,
413
+ mcpEndpoint: () => mcpEndpoint,
414
+ normalizeRepo: () => normalizeRepo,
415
+ reviewCutoffAt: () => reviewCutoffAt
33
416
  });
34
417
  import { homedir } from "os";
35
- import { join, resolve } from "path";
36
- import { mkdirSync, readFileSync, realpathSync } from "fs";
418
+ import { basename, join, resolve } from "path";
419
+ import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
37
420
  function normalizeRepo(repo) {
38
421
  const r = (repo ?? "").trim();
39
422
  if (!r) return "";
@@ -43,12 +426,20 @@ function normalizeRepo(repo) {
43
426
  return resolve(r);
44
427
  }
45
428
  }
429
+ function droneWorktreePath(worktreesDir, repo, slug) {
430
+ return join(worktreesDir, basename(normalizeRepo(repo)), slug);
431
+ }
46
432
  function envNum(name, def) {
47
433
  const raw = process.env[name];
48
434
  if (raw === void 0 || raw === "") return def;
49
435
  const n = Number(raw);
50
436
  return Number.isFinite(n) && n >= 0 ? n : def;
51
437
  }
438
+ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
439
+ const base2 = api.replace(/\/$/, "");
440
+ const p = path.startsWith("/") ? path : `/${path}`;
441
+ return `${base2}${p}`;
442
+ }
52
443
  function tokenFromFile() {
53
444
  try {
54
445
  return readFileSync(join(SWARM_HOME, "token"), "utf8").trim();
@@ -56,60 +447,75 @@ function tokenFromFile() {
56
447
  return "";
57
448
  }
58
449
  }
59
- var SWARM_HOME, DB_PATH, WORKTREES_DIR, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, TASKLESS_API, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI;
450
+ function reviewCutoffAt() {
451
+ try {
452
+ const raw = JSON.parse(readFileSync(REVIEW_CUTOFF_PATH, "utf8"));
453
+ if (raw.at) return raw.at;
454
+ } catch {
455
+ }
456
+ const at = stamp();
457
+ try {
458
+ const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
459
+ writeFileSync(tmp, JSON.stringify({ at }));
460
+ renameSync(tmp, REVIEW_CUTOFF_PATH);
461
+ } catch {
462
+ }
463
+ return at;
464
+ }
465
+ var SWARM_HOME, DB_PATH, WORKTREES_DIR, RUN_LOGS_DIR, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_REHYDRATE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_SWEEP_INTERVAL_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_REPO_PATHS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS;
60
466
  var init_config = __esm({
61
467
  "src/config.ts"() {
62
468
  "use strict";
469
+ init_src();
63
470
  SWARM_HOME = process.env.SWARM_HOME || join(homedir(), ".taskless-swarm");
64
471
  mkdirSync(SWARM_HOME, { recursive: true });
65
472
  DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
66
473
  WORKTREES_DIR = process.env.SWARM_WORKTREES || join(SWARM_HOME, "worktrees");
67
- SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : "sqlite";
474
+ RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
475
+ DIST_BUILD = true;
476
+ SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
68
477
  SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
69
478
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
70
479
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
480
+ SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
481
+ SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
482
+ SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
483
+ SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
484
+ SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
485
+ SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
486
+ SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
487
+ SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
488
+ SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
71
489
  HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
72
490
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
73
491
  SWARM_REPO = process.env.SWARM_REPO || "";
492
+ SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
74
493
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
494
+ SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
495
+ TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
75
496
  TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
76
497
  TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
77
498
  TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
78
499
  TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
500
+ SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
501
+ SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
502
+ REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
503
+ SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
79
504
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
80
505
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
81
506
  WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
507
+ SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
508
+ SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
509
+ SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
510
+ SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
511
+ SWARM_ANOMALY_PR_STUCK_MERGE_MS = envNum("SWARM_ANOMALY_PR_STUCK_MERGE_MS", 30 * 6e4);
512
+ SWARM_ANOMALY_PR_NO_REVIEWER_MS = envNum("SWARM_ANOMALY_PR_NO_REVIEWER_MS", 20 * 6e4);
513
+ SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
514
+ SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
82
515
  }
83
516
  });
84
517
 
85
518
  // src/names.ts
86
- function toRoman(n) {
87
- if (n < 1 || n > 3999) return String(n);
88
- const table = [
89
- [1e3, "M"],
90
- [900, "CM"],
91
- [500, "D"],
92
- [400, "CD"],
93
- [100, "C"],
94
- [90, "XC"],
95
- [50, "L"],
96
- [40, "XL"],
97
- [10, "X"],
98
- [9, "IX"],
99
- [5, "V"],
100
- [4, "IV"],
101
- [1, "I"]
102
- ];
103
- let out = "";
104
- for (const [v, sym] of table) while (n >= v) out += sym, n -= v;
105
- return out;
106
- }
107
- function genName(base2, g) {
108
- return g <= 1 ? base2 : `${base2} ${toRoman(g)}`;
109
- }
110
- function baseName(name) {
111
- return name.split(" ")[0];
112
- }
113
519
  function pickName(rows) {
114
520
  const live = /* @__PURE__ */ new Set();
115
521
  const lastUse = /* @__PURE__ */ new Map();
@@ -129,39 +535,12 @@ function pickName(rows) {
129
535
  }
130
536
  }
131
537
  }
132
- var DRONE_POOL, NAME_LIVE;
133
- var init_names = __esm({
538
+ var NAME_LIVE;
539
+ var init_names2 = __esm({
134
540
  "src/names.ts"() {
135
541
  "use strict";
136
- DRONE_POOL = [
137
- // Латинский бестиарий (15)
138
- "SCARAB",
139
- "VESPA",
140
- "MANTIS",
141
- "HORNET",
142
- "CICADA",
143
- "LOCUST",
144
- "ATTA",
145
- "FORMICA",
146
- "MYRMEX",
147
- "APIS",
148
- "TERMES",
149
- "GRYLLUS",
150
- "LAMPYRIS",
151
- "ARACHNE",
152
- "SPHINX",
153
- // Мифо-стаи (9)
154
- "TIAMAT",
155
- "JORMUNGAND",
156
- "GRENDEL",
157
- "TYPHON",
158
- "FENRIS",
159
- "GARM",
160
- "KUKULKAN",
161
- "LEVIATHAN",
162
- "SURTUR"
163
- ];
164
- NAME_LIVE = /* @__PURE__ */ new Set(["queued", "running", "blocked", "review"]);
542
+ init_src();
543
+ NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
165
544
  }
166
545
  });
167
546
 
@@ -169,9 +548,6 @@ var init_names = __esm({
169
548
  function resolveStormConfig(cfg) {
170
549
  return { ...DEFAULT_STORM_CONFIG, ...cfg ?? {} };
171
550
  }
172
- function sqliteMs(ts) {
173
- return Date.parse(ts.replace(" ", "T") + "Z");
174
- }
175
551
  function encodeProbe(rec) {
176
552
  return JSON.stringify(rec);
177
553
  }
@@ -228,6 +604,7 @@ var numEnv, DEFAULT_STORM_CONFIG, STORM_START_TEXT, STORM_END_TEXT, STORM_DEAD_T
228
604
  var init_storm = __esm({
229
605
  "src/storm.ts"() {
230
606
  "use strict";
607
+ init_src();
231
608
  numEnv = (name, def) => {
232
609
  const v = Number(process.env[name]);
233
610
  return Number.isFinite(v) && v > 0 ? v : def;
@@ -253,7 +630,7 @@ var db_exports = {};
253
630
  __export(db_exports, {
254
631
  SwarmDb: () => SwarmDb
255
632
  });
256
- import Database from "better-sqlite3";
633
+ import { createRequire } from "module";
257
634
  import { existsSync } from "fs";
258
635
  import { resolve as resolve2 } from "path";
259
636
  function rowToQuestion(row) {
@@ -295,6 +672,8 @@ function rowToRun(row) {
295
672
  respawns: row.respawns ?? 0,
296
673
  // старые прогоны без колонки → 0
297
674
  resolves_run_id: row.resolves_run_id ?? null,
675
+ reviews_run_id: row.reviews_run_id ?? null,
676
+ review_verdict: row.review_verdict ?? null,
298
677
  task_title: row.task_title ?? null,
299
678
  task_desc: row.task_desc ?? null,
300
679
  task_url: row.task_url ?? null,
@@ -302,13 +681,26 @@ function rowToRun(row) {
302
681
  updated_at: row.updated_at
303
682
  };
304
683
  }
305
- var SCHEMA, SwarmDb;
684
+ function loadBetterSqlite3() {
685
+ const require2 = createRequire(import.meta.url);
686
+ try {
687
+ return require2("better-sqlite3");
688
+ } catch (err) {
689
+ throw new Error(
690
+ `better-sqlite3 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. Sqlite-\u0440\u0435\u0436\u0438\u043C (SwarmDb) \u2014 \u0434\u0435\u0444\u043E\u043B\u0442 \u043C\u043E\u043D\u043E\u0440\u0435\u043F\u044B/\u0442\u0435\u0441\u0442\u043E\u0432; \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u0438 sqlite \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 optional dependency (npm i, \u0431\u0435\u0437 --omit=optional) \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_BACKEND=client. \u0418\u0441\u0445\u043E\u0434\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430: ${err.message}`
691
+ );
692
+ }
693
+ }
694
+ var TERMINAL_SQL, RESOLVER_TERMINAL_SQL, SCHEMA, SwarmDb;
306
695
  var init_db = __esm({
307
696
  "src/db.ts"() {
308
697
  "use strict";
309
698
  init_config();
310
- init_names();
699
+ init_names2();
311
700
  init_storm();
701
+ init_src();
702
+ TERMINAL_SQL = TERMINAL_STATUSES.map((s) => `'${s}'`).join(",");
703
+ RESOLVER_TERMINAL_SQL = [...TERMINAL_STATUSES, "stalled"].map((s) => `'${s}'`).join(",");
312
704
  SCHEMA = `
313
705
  CREATE TABLE IF NOT EXISTS runs (
314
706
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -331,6 +723,8 @@ CREATE TABLE IF NOT EXISTS runs (
331
723
  stop_reason TEXT,
332
724
  respawns INTEGER NOT NULL DEFAULT 0,
333
725
  resolves_run_id INTEGER,
726
+ reviews_run_id INTEGER,
727
+ review_verdict TEXT,
334
728
  task_title TEXT,
335
729
  task_desc TEXT,
336
730
  task_url TEXT,
@@ -372,9 +766,12 @@ CREATE TABLE IF NOT EXISTS imports (
372
766
  SwarmDb = class {
373
767
  db;
374
768
  defaultRepo;
769
+ databaseCtor;
770
+ // для importDb (второй Database, source)
375
771
  constructor(path = "swarm.db", defaultRepo = "") {
376
772
  this.defaultRepo = normalizeRepo(defaultRepo);
377
- this.db = new Database(path);
773
+ this.databaseCtor = loadBetterSqlite3();
774
+ this.db = new this.databaseCtor(path);
378
775
  this.db.exec("PRAGMA journal_mode = WAL;");
379
776
  this.db.exec(SCHEMA);
380
777
  for (const alter of [
@@ -395,7 +792,11 @@ CREATE TABLE IF NOT EXISTS imports (
395
792
  // Иерархия роя (TASWM-78): роль прогона (drone|hero) и ссылка на прогон-героя.
396
793
  "ALTER TABLE runs ADD COLUMN kind TEXT NOT NULL DEFAULT 'drone'",
397
794
  "ALTER TABLE runs ADD COLUMN hero_run_id INTEGER",
398
- "ALTER TABLE events ADD COLUMN actor TEXT"
795
+ "ALTER TABLE events ADD COLUMN actor TEXT",
796
+ // Авто-ревью (TASWM-148): прогон-ревьюер (reviews_run_id → review-прогон) и
797
+ // вердикт (approve|changes), который ревьюер пишет на проверяемый прогон.
798
+ "ALTER TABLE runs ADD COLUMN reviews_run_id INTEGER",
799
+ "ALTER TABLE runs ADD COLUMN review_verdict TEXT"
399
800
  ]) {
400
801
  try {
401
802
  this.db.exec(alter);
@@ -415,7 +816,7 @@ CREATE TABLE IF NOT EXISTS imports (
415
816
  createRun(input) {
416
817
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
417
818
  const res = this.db.prepare(
418
- "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
819
+ "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id, reviews_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
419
820
  ).run(
420
821
  input.task_slug,
421
822
  name,
@@ -429,10 +830,11 @@ CREATE TABLE IF NOT EXISTS imports (
429
830
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
430
831
  input.model ?? null,
431
832
  input.decision ? JSON.stringify(input.decision) : null,
432
- input.resolves_run_id ?? null
833
+ input.resolves_run_id ?? null,
834
+ input.reviews_run_id ?? null
433
835
  );
434
836
  const id = Number(res.lastInsertRowid);
435
- const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
837
+ const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
436
838
  this.addEvent(id, "spawned", spawnNote);
437
839
  return this.getRun(id);
438
840
  }
@@ -485,7 +887,7 @@ CREATE TABLE IF NOT EXISTS imports (
485
887
  */
486
888
  activeResolverFor(reviewRunId) {
487
889
  const row = this.db.prepare(
488
- "SELECT * FROM runs WHERE resolves_run_id = ? AND status NOT IN ('done','failed','superseded','stopped','stalled') ORDER BY id DESC LIMIT 1"
890
+ `SELECT * FROM runs WHERE resolves_run_id = ? AND status NOT IN (${RESOLVER_TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
489
891
  ).get(reviewRunId);
490
892
  return row ? rowToRun(row) : null;
491
893
  }
@@ -494,10 +896,48 @@ CREATE TABLE IF NOT EXISTS imports (
494
896
  const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
495
897
  return row.n;
496
898
  }
899
+ /**
900
+ * Сколько ЖИВЫХ/УСПЕШНЫХ ревьюеров уже поднимали на этот review-PR (авто-ревью, TASWM-148) —
901
+ * one-shot идемпотентность БЕЗ лимита попыток (в отличие от резолвера): >0 → повторно не
902
+ * спавним. failed/stalled ревьюеры (worktree/launch сорвались ДО вердикта) в счёт НЕ идут —
903
+ * иначе один транзиентный сбой спавна (диск/git/gh) навсегда блокировал бы ревью этого PR без
904
+ * единого шанса на повтор — тихо нарушая политику владельца «каждый PR ревьюится».
905
+ */
906
+ reviewerCountFor(reviewRunId) {
907
+ const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status NOT IN ('failed','stalled')").get(reviewRunId);
908
+ return row.n;
909
+ }
910
+ /**
911
+ * Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
912
+ * ВСЕГО (любой статус, включая failed/stalled — специально в отличие от reviewerCountFor
913
+ * выше) когда-либо поднималось на review-прогон #reviewRunId. Зеркалит resolverCountFor —
914
+ * плоский потолок попыток. Именно это ловит карусель: reviewerCountFor исключает
915
+ * failed/stalled (чтобы ОДИН транзиентный сбой не блокировал ревью навсегда), но тем
916
+ * самым допускает бесконечный ретрай, если ревьюер стабильно умирает без done().
917
+ */
918
+ reviewerAttemptCountFor(reviewRunId) {
919
+ const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ?").get(reviewRunId);
920
+ return row.n;
921
+ }
922
+ /** Последняя (любой статус) попытка ревьюера на review-прогон #reviewRunId (TASWM-196) —
923
+ * см. store.ts:lastReviewerAttemptFor. */
924
+ lastReviewerAttemptFor(reviewRunId) {
925
+ const row = this.db.prepare("SELECT * FROM runs WHERE reviews_run_id = ? ORDER BY id DESC LIMIT 1").get(reviewRunId);
926
+ return row ? rowToRun(row) : null;
927
+ }
928
+ /**
929
+ * Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
930
+ * closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
931
+ * не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
932
+ */
933
+ activeReviewersFor(reviewRunId) {
934
+ const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
935
+ return rows.map(rowToRun);
936
+ }
497
937
  /** Последний живой (не терминальный) прогон по slug — для swarm cancel. */
498
938
  activeRunForSlug(slug) {
499
939
  const row = this.db.prepare(
500
- "SELECT * FROM runs WHERE task_slug = ? AND status NOT IN ('done','failed','superseded','stopped') ORDER BY id DESC LIMIT 1"
940
+ `SELECT * FROM runs WHERE task_slug = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
501
941
  ).get(slug);
502
942
  return row ? rowToRun(row) : null;
503
943
  }
@@ -554,7 +994,7 @@ CREATE TABLE IF NOT EXISTS imports (
554
994
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
555
995
  if (already) return { imported: false, runs: already.run_count };
556
996
  if (!existsSync(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
557
- const src = new Database(abs);
997
+ const src = new this.databaseCtor(abs);
558
998
  try {
559
999
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
560
1000
  const events = src.prepare("SELECT * FROM events ORDER BY id").all();
@@ -729,11 +1169,10 @@ CREATE TABLE IF NOT EXISTS imports (
729
1169
  taskUrl: row.taskUrl ?? null
730
1170
  });
731
1171
  }
732
- const MILESTONES2 = /* @__PURE__ */ new Set(["spawned", "pr_opened", "merged", "done", "failed", "stalled", "conflict", "stopped", "superseded", "paused", "resumed", "storm_start", "storm_end", "storm_dead"]);
733
1172
  for (const row of this.db.prepare(
734
1173
  "SELECT e.*, r.task_slug slug, r.name name, r.repo repo, r.task_title title, r.pr_url pr, r.task_url taskUrl FROM events e LEFT JOIN runs r ON r.id = e.run_id"
735
1174
  ).all()) {
736
- if (!MILESTONES2.has(row.kind)) continue;
1175
+ if (!isMilestoneKind(row.kind)) continue;
737
1176
  const global = row.run_id == null;
738
1177
  items.push({
739
1178
  kind: "milestone",
@@ -794,12 +1233,13 @@ CREATE TABLE IF NOT EXISTS imports (
794
1233
  return row.n;
795
1234
  }
796
1235
  /**
797
- * Спавн-фейлов (событий failed на НЕ-резолверных прогонах) с момента cutoff.
798
- * Резолверные failed (merge-main-in) не считаем штормовым сигналом.
1236
+ * Спавн-фейлов (событий failed на НЕ-резолверных/НЕ-ревьюерных прогонах) с момента
1237
+ * cutoff. Резолверные (merge-main-in) и ревьюерные (авто-ревью, TASWM-148) failed не
1238
+ * считаем штормовым сигналом.
799
1239
  */
800
1240
  spawnFailCountSince(cutoffSeconds) {
801
1241
  const row = this.db.prepare(
802
- "SELECT COUNT(*) n FROM events e JOIN runs r ON r.id = e.run_id WHERE e.kind='failed' AND r.resolves_run_id IS NULL AND e.at >= datetime(?, 'unixepoch')"
1242
+ "SELECT COUNT(*) n FROM events e JOIN runs r ON r.id = e.run_id WHERE e.kind='failed' AND r.resolves_run_id IS NULL AND r.reviews_run_id IS NULL AND e.at >= datetime(?, 'unixepoch')"
803
1243
  ).get(cutoffSeconds);
804
1244
  return row.n;
805
1245
  }
@@ -823,7 +1263,7 @@ CREATE TABLE IF NOT EXISTS imports (
823
1263
 
824
1264
  // src/oauth.ts
825
1265
  import { createHash, randomBytes } from "crypto";
826
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync } from "fs";
1266
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "fs";
827
1267
  import { join as join4 } from "path";
828
1268
  function makePkce() {
829
1269
  const verifier = randomBytes(48).toString("base64url");
@@ -908,13 +1348,13 @@ function toStore(clientId, t, now) {
908
1348
  }
909
1349
  function loadStore() {
910
1350
  try {
911
- return JSON.parse(readFileSync4(STORE_PATH, "utf8"));
1351
+ return JSON.parse(readFileSync6(STORE_PATH, "utf8"));
912
1352
  } catch {
913
1353
  return null;
914
1354
  }
915
1355
  }
916
1356
  function saveStore(s) {
917
- writeFileSync4(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
1357
+ writeFileSync5(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
918
1358
  }
919
1359
  function clearStore() {
920
1360
  try {
@@ -924,7 +1364,7 @@ function clearStore() {
924
1364
  }
925
1365
  function loadClientId() {
926
1366
  try {
927
- return JSON.parse(readFileSync4(CLIENT_PATH, "utf8")).clientId;
1367
+ return JSON.parse(readFileSync6(CLIENT_PATH, "utf8")).clientId;
928
1368
  } catch {
929
1369
  return null;
930
1370
  }
@@ -933,7 +1373,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
933
1373
  const existing = loadClientId();
934
1374
  if (existing) return existing;
935
1375
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
936
- writeFileSync4(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
1376
+ writeFileSync5(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
937
1377
  return clientId;
938
1378
  }
939
1379
  async function ensureAccess(store, opts = {}) {
@@ -966,11 +1406,11 @@ var init_oauth = __esm({
966
1406
  });
967
1407
 
968
1408
  // src/workspace.ts
969
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
1409
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
970
1410
  import { join as join5 } from "path";
971
1411
  function loadWorkspace() {
972
1412
  try {
973
- const w = JSON.parse(readFileSync5(WORKSPACE_PATH, "utf8"));
1413
+ const w = JSON.parse(readFileSync7(WORKSPACE_PATH, "utf8"));
974
1414
  return w && typeof w.id === "string" && w.id ? w : null;
975
1415
  } catch {
976
1416
  return null;
@@ -991,9 +1431,14 @@ var init_workspace = __esm({
991
1431
  // src/taskless.ts
992
1432
  var taskless_exports = {};
993
1433
  __export(taskless_exports, {
1434
+ TASK_CONTEXT_MAX_COMMENTS: () => TASK_CONTEXT_MAX_COMMENTS,
1435
+ TASK_CONTEXT_MAX_COMMENT_CHARS: () => TASK_CONTEXT_MAX_COMMENT_CHARS,
1436
+ TASK_CONTEXT_MAX_DOCS: () => TASK_CONTEXT_MAX_DOCS,
1437
+ TASK_CONTEXT_MAX_DOC_CHARS: () => TASK_CONTEXT_MAX_DOC_CHARS,
994
1438
  connectSwarmMcp: () => connectSwarmMcp,
995
1439
  connectTaskless: () => connectTaskless,
996
1440
  extractText: () => extractText,
1441
+ fetchTaskContext: () => fetchTaskContext,
997
1442
  makeTasklessClient: () => makeTasklessClient
998
1443
  });
999
1444
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -1021,12 +1466,25 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
1021
1466
  description: extractText(task.description),
1022
1467
  url: taskUrlBase ? `${taskUrlBase}/tasks/${slug}` : null,
1023
1468
  parentId: task.parentId ?? null,
1024
- statusCategory: task.status?.category ?? null
1469
+ projectId: task.projectId ?? null,
1470
+ statusCategory: task.status?.category ?? null,
1471
+ comments: (task.comments ?? []).map((c) => ({
1472
+ author: c.author?.displayName ?? "\u2014",
1473
+ createdAt: (c.createdAt ?? "").slice(0, 16).replace("T", " "),
1474
+ text: extractText(c.body)
1475
+ })),
1476
+ documents: (task.documents ?? []).filter((d) => !!d.id).map((d) => ({ id: d.id, title: d.title ?? d.id, url: d.url ?? null }))
1025
1477
  };
1026
1478
  idCache.set(slug, body.id);
1027
1479
  idCache.set(body.id, body.id);
1028
1480
  return body;
1029
1481
  }
1482
+ async function getDocument(id) {
1483
+ const res = await safeCall("get_document", { id });
1484
+ const doc = res?.structuredContent?.document ?? null;
1485
+ if (!doc) return null;
1486
+ return { title: doc.title ?? "", body: extractText(doc.body) };
1487
+ }
1030
1488
  async function resolveId(idOrSlug) {
1031
1489
  const cached = idCache.get(idOrSlug);
1032
1490
  if (cached) return cached;
@@ -1056,13 +1514,18 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
1056
1514
  if (id) await safeCall("add_comment", { taskId: id, body });
1057
1515
  },
1058
1516
  backlogStatusId,
1517
+ getDocument,
1059
1518
  createTask: async (input) => {
1519
+ if (!input.projectId) {
1520
+ throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
1521
+ }
1060
1522
  const res = await safeCall("create_task", {
1061
- parentId: input.parentId,
1523
+ ...input.parentId ? { parentId: input.parentId } : {},
1062
1524
  title: input.title,
1063
1525
  description: input.description,
1064
- estimateHours: input.estimateHours,
1065
- statusId: input.statusId,
1526
+ ...input.estimateHours != null ? { estimateHours: input.estimateHours } : {},
1527
+ ...input.statusId ? { statusId: input.statusId } : {},
1528
+ projectId: input.projectId,
1066
1529
  force: true
1067
1530
  });
1068
1531
  const task = res?.structuredContent?.task ?? null;
@@ -1074,6 +1537,44 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
1074
1537
  }
1075
1538
  };
1076
1539
  }
1540
+ async function fetchTaskContext(client, task) {
1541
+ const parts = [];
1542
+ const comments = task.comments ?? [];
1543
+ if (comments.length > 0) {
1544
+ const tail = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
1545
+ const skipped = comments.length - tail.length;
1546
+ const lines = tail.map((c) => {
1547
+ const cut = c.text.length > TASK_CONTEXT_MAX_COMMENT_CHARS;
1548
+ const text = cut ? `${c.text.slice(0, TASK_CONTEXT_MAX_COMMENT_CHARS)}\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)` : c.text;
1549
+ return `\u2014 [${c.createdAt} \xB7 ${c.author}] ${text}`;
1550
+ });
1551
+ const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
1552
+ parts.push([header, ...lines].join("\n"));
1553
+ }
1554
+ const documents = task.documents ?? [];
1555
+ if (documents.length > 0) {
1556
+ const head = documents.slice(0, TASK_CONTEXT_MAX_DOCS);
1557
+ const rest = documents.slice(TASK_CONTEXT_MAX_DOCS);
1558
+ const renderedHead = await Promise.all(
1559
+ head.map(async (d) => {
1560
+ const doc = await client.getDocument(d.id);
1561
+ if (!doc) return `### ${d.title}${d.url ? ` (${d.url})` : ""}
1562
+ (\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442)`;
1563
+ const cut = doc.body.length > TASK_CONTEXT_MAX_DOC_CHARS;
1564
+ const text = cut ? `${doc.body.slice(0, TASK_CONTEXT_MAX_DOC_CHARS)}\u2026` : doc.body;
1565
+ return `### ${d.title}${d.url ? ` (${d.url})` : ""}${cut ? " \u2014 \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435/get_document" : ""}
1566
+ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
1567
+ })
1568
+ );
1569
+ const restLines = rest.map((d) => `- ${d.title}${d.url ? ` (${d.url})` : ""} \u2014 \u0442\u0435\u043B\u043E \u043D\u0435 \u043F\u043E\u0434\u0442\u044F\u043D\u0443\u0442\u043E, \u0447\u0438\u0442\u0430\u0439 \u0447\u0435\u0440\u0435\u0437 get_document/read_task`);
1570
+ parts.push(
1571
+ [`\u041F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0435 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B (${documents.length}):`, ...renderedHead, ...restLines.length ? [restLines.join("\n")] : []].join(
1572
+ "\n\n"
1573
+ )
1574
+ );
1575
+ }
1576
+ return parts.join("\n\n");
1577
+ }
1077
1578
  function parseJsonContent(res) {
1078
1579
  const text = res?.content?.find((c) => c.type === "text")?.text;
1079
1580
  if (!text) return null;
@@ -1100,13 +1601,13 @@ async function connectTaskless(cfg) {
1100
1601
  }
1101
1602
  if (!token) token = TASKLESS_TOKEN;
1102
1603
  if (!token) return null;
1103
- const url = new URL(`${api.replace(/\/$/, "")}/api/mcp`);
1604
+ const url = new URL(mcpEndpoint(api));
1104
1605
  const transport = new StreamableHTTPClientTransport(url, {
1105
1606
  requestInit: { headers: { Authorization: `Bearer ${token}` } }
1106
1607
  });
1107
1608
  const client = new Client({ name: "taskless-swarm", version: "0.1.0" }, { capabilities: {} });
1108
1609
  try {
1109
- await client.connect(transport);
1610
+ await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
1110
1611
  } catch (e) {
1111
1612
  console.warn(`Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`);
1112
1613
  return null;
@@ -1131,7 +1632,7 @@ async function connectSwarmMcp(cfg) {
1131
1632
  }
1132
1633
  if (!token) token = TASKLESS_TOKEN;
1133
1634
  if (!token) return null;
1134
- const url = new URL(`${api.replace(/\/$/, "")}/api/mcp`);
1635
+ const url = new URL(mcpEndpoint(api));
1135
1636
  const transport = new StreamableHTTPClientTransport(url, {
1136
1637
  requestInit: { headers: { Authorization: `Bearer ${token}` } }
1137
1638
  });
@@ -1144,71 +1645,158 @@ async function connectSwarmMcp(cfg) {
1144
1645
  }
1145
1646
  return async (name, args) => await client.callTool({ name, arguments: { ...args, ...ws } });
1146
1647
  }
1147
- function extractText(d) {
1148
- if (typeof d === "string") return d;
1149
- const out = [];
1150
- const walk = (n) => {
1151
- if (!n || typeof n !== "object") return;
1152
- const node = n;
1153
- if (node.text) out.push(node.text);
1154
- (node.content ?? []).forEach(walk);
1155
- };
1156
- walk(d);
1157
- return out.join(" ");
1158
- }
1159
- var SLUG_RE;
1648
+ var SLUG_RE, TASK_CONTEXT_MAX_DOCS, TASK_CONTEXT_MAX_DOC_CHARS, TASK_CONTEXT_MAX_COMMENTS, TASK_CONTEXT_MAX_COMMENT_CHARS, TASKLESS_CONNECT_TIMEOUT_MS;
1160
1649
  var init_taskless = __esm({
1161
1650
  "src/taskless.ts"() {
1162
1651
  "use strict";
1652
+ init_src();
1163
1653
  init_config();
1164
1654
  init_oauth();
1165
1655
  init_workspace();
1166
1656
  SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
1657
+ TASK_CONTEXT_MAX_DOCS = 5;
1658
+ TASK_CONTEXT_MAX_DOC_CHARS = 4e3;
1659
+ TASK_CONTEXT_MAX_COMMENTS = 30;
1660
+ TASK_CONTEXT_MAX_COMMENT_CHARS = 2e3;
1661
+ TASKLESS_CONNECT_TIMEOUT_MS = 1e4;
1167
1662
  }
1168
1663
  });
1169
1664
 
1170
1665
  // src/overmind.ts
1171
1666
  init_db();
1172
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1667
+ import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
1173
1668
  import { basename as basename3, dirname as dirname2, join as join7 } from "path";
1174
1669
  import { fileURLToPath, pathToFileURL } from "url";
1670
+ import { execFileSync as execFileSync3 } from "child_process";
1671
+
1672
+ // src/swarmClient.ts
1673
+ init_config();
1674
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1675
+ import { dirname } from "path";
1676
+ import { randomUUID } from "crypto";
1677
+
1678
+ // src/resources.ts
1679
+ init_config();
1680
+ import os from "os";
1681
+ import { execFileSync } from "child_process";
1682
+ import { readFileSync as readFileSync2 } from "fs";
1683
+ function isTransientResourceError(e) {
1684
+ const code = e?.code;
1685
+ return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
1686
+ }
1687
+ function logTransientSpawnError(label, e) {
1688
+ if (isTransientResourceError(e)) {
1689
+ const code = e.code;
1690
+ console.warn(`[overmind] ${label}: \u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (${code}) \u2014 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u044E \u0441\u0438\u0433\u043D\u0430\u043B \u044D\u0442\u043E\u0433\u043E \u0442\u0438\u043A\u0430, \u043D\u0435 \u043F\u0430\u0434\u0430\u044E`);
1691
+ } else {
1692
+ console.warn(`[overmind] ${label} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F (\u043D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u0437\u0430 \u044D\u0442\u043E\u0442 \u0442\u0438\u043A): ${String(e)}`);
1693
+ }
1694
+ }
1695
+ function resourcesOk(load1, cpus, freeGb, maxLoadFactor = SWARM_MAX_LOAD_FACTOR, minFreeGb = SWARM_MIN_FREE_GB, fdLoadRatio = null, maxFdLoadFactor = SWARM_MAX_FD_LOAD_FACTOR) {
1696
+ const cores = cpus > 0 ? cpus : 1;
1697
+ const fdsOk = fdLoadRatio == null || fdLoadRatio <= maxFdLoadFactor;
1698
+ return load1 <= cores * maxLoadFactor && freeGb >= minFreeGb && fdsOk;
1699
+ }
1700
+ function parseVmStatAvailableBytes(vmStatOut) {
1701
+ const page = Number(vmStatOut.match(/page size of (\d+) bytes/)?.[1]) || 4096;
1702
+ const pages = (label) => {
1703
+ const m = vmStatOut.match(new RegExp(`Pages ${label}:\\s+(\\d+)`));
1704
+ return m ? Number(m[1]) : 0;
1705
+ };
1706
+ return (pages("free") + pages("inactive") + pages("speculative") + pages("purgeable")) * page;
1707
+ }
1708
+ function availableMemBytes() {
1709
+ if (process.platform === "darwin") {
1710
+ try {
1711
+ const bytes = parseVmStatAvailableBytes(execFileSync("vm_stat", { encoding: "utf8" }));
1712
+ if (bytes > 0) return bytes;
1713
+ } catch (e) {
1714
+ logTransientSpawnError("vm_stat", e);
1715
+ }
1716
+ }
1717
+ return os.freemem();
1718
+ }
1719
+ function parseSysctlInt(out) {
1720
+ const n = Number(out.trim());
1721
+ return Number.isFinite(n) && n > 0 ? n : null;
1722
+ }
1723
+ function readSysctlPair(nameA, nameB) {
1724
+ try {
1725
+ const lines = execFileSync("sysctl", ["-n", nameA, nameB], { encoding: "utf8" }).trim().split("\n");
1726
+ return [parseSysctlInt(lines[0] ?? ""), parseSysctlInt(lines[1] ?? "")];
1727
+ } catch (e) {
1728
+ logTransientSpawnError(`sysctl ${nameA}/${nameB}`, e);
1729
+ return [null, null];
1730
+ }
1731
+ }
1732
+ function fileDescriptorLoadRatio() {
1733
+ if (process.platform === "darwin") {
1734
+ const [max, used] = readSysctlPair("kern.maxfiles", "kern.num_files");
1735
+ return max != null && used != null ? used / max : null;
1736
+ }
1737
+ if (process.platform === "linux") {
1738
+ try {
1739
+ const [allocated, , max] = readFileSync2("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
1740
+ if (Number.isFinite(allocated ?? NaN) && Number.isFinite(max ?? NaN) && max) return allocated / max;
1741
+ } catch {
1742
+ }
1743
+ }
1744
+ return null;
1745
+ }
1746
+ var RESOURCES_CACHE_MS = 250;
1747
+ var readingCache = null;
1748
+ function hostReading() {
1749
+ const now = Date.now();
1750
+ if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
1751
+ const reading = {
1752
+ load1: os.loadavg()[0] ?? 0,
1753
+ cpus: os.cpus().length,
1754
+ freeGb: availableMemBytes() / 1e9,
1755
+ fdLoadRatio: fileDescriptorLoadRatio()
1756
+ };
1757
+ readingCache = { at: now, reading };
1758
+ return reading;
1759
+ }
1760
+ function hostResourcesOk() {
1761
+ const r = hostReading();
1762
+ return resourcesOk(r.load1, r.cpus, r.freeGb, void 0, void 0, r.fdLoadRatio);
1763
+ }
1764
+ function hostReliefNeeded() {
1765
+ const r = hostReading();
1766
+ return !resourcesOk(r.load1, r.cpus, r.freeGb, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, r.fdLoadRatio, SWARM_RELIEF_MAX_FD_LOAD_FACTOR);
1767
+ }
1175
1768
 
1176
1769
  // src/swarmClient.ts
1177
- init_config();
1178
- init_names();
1770
+ init_names2();
1179
1771
  init_storm();
1180
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1181
- import { dirname } from "path";
1182
- import { randomUUID } from "crypto";
1183
- function stamp(d = /* @__PURE__ */ new Date()) {
1184
- return d.toISOString().slice(0, 19).replace("T", " ");
1772
+ init_src();
1773
+ var RESOLVER_TERMINAL = /* @__PURE__ */ new Set([...TERMINAL_STATUSES, "stalled"]);
1774
+ var SLUG_TERMINAL = new Set(TERMINAL_STATUSES);
1775
+ function runRef(runKey) {
1776
+ return { __runRef: runKey };
1185
1777
  }
1186
- function stampFromEpochSec(sec) {
1187
- return stamp(new Date(sec * 1e3));
1778
+ function isRunRef(v) {
1779
+ return typeof v === "object" && v !== null && "__runRef" in v;
1780
+ }
1781
+ function jsonFromTextContent(res) {
1782
+ const text = res?.content?.find((c) => c.type === "text")?.text;
1783
+ if (text) {
1784
+ try {
1785
+ return JSON.parse(text);
1786
+ } catch {
1787
+ }
1788
+ }
1789
+ return res?.structuredContent ?? null;
1188
1790
  }
1189
- var RESOLVER_TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "superseded", "stopped", "stalled"]);
1190
- var SLUG_TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "superseded", "stopped"]);
1191
- var MILESTONES = /* @__PURE__ */ new Set([
1192
- "spawned",
1193
- "pr_opened",
1194
- "merged",
1195
- "done",
1196
- "failed",
1197
- "stalled",
1198
- "conflict",
1199
- "stopped",
1200
- "superseded",
1201
- "paused",
1202
- "resumed",
1203
- "storm_start",
1204
- "storm_end",
1205
- "storm_dead"
1206
- ]);
1207
1791
  var SwarmClient = class _SwarmClient {
1208
1792
  call;
1209
1793
  defaultRepo;
1210
1794
  autoFlush;
1211
1795
  listLimit;
1796
+ // TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
1797
+ // карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
1798
+ repoFullNames;
1799
+ repoPathByFullName;
1212
1800
  // Зеркало.
1213
1801
  runs = /* @__PURE__ */ new Map();
1214
1802
  runtime = /* @__PURE__ */ new Map();
@@ -1227,6 +1815,8 @@ var SwarmClient = class _SwarmClient {
1227
1815
  // Outbox / flush.
1228
1816
  outboxPath;
1229
1817
  mapPath;
1818
+ stormPath;
1819
+ // durable-сайдкар глобальных (штормовых) событий
1230
1820
  pending = [];
1231
1821
  nextSeq = 1;
1232
1822
  flushing = false;
@@ -1239,8 +1829,12 @@ var SwarmClient = class _SwarmClient {
1239
1829
  this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
1240
1830
  this.autoFlush = opts.autoFlush ?? true;
1241
1831
  this.listLimit = opts.listLimit ?? 200;
1832
+ const registry = opts.repoRegistry ?? [];
1833
+ this.repoFullNames = registry.map((e) => e.repoFullName);
1834
+ this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
1242
1835
  this.outboxPath = opts.outboxPath;
1243
1836
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
1837
+ this.stormPath = `${opts.outboxPath}.storm.jsonl`;
1244
1838
  mkdirSync2(dirname(this.outboxPath), { recursive: true });
1245
1839
  this.recover();
1246
1840
  }
@@ -1248,13 +1842,13 @@ var SwarmClient = class _SwarmClient {
1248
1842
  recover() {
1249
1843
  if (existsSync2(this.mapPath)) {
1250
1844
  try {
1251
- const obj = JSON.parse(readFileSync2(this.mapPath, "utf8"));
1845
+ const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
1252
1846
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
1253
1847
  } catch {
1254
1848
  }
1255
1849
  }
1256
1850
  if (existsSync2(this.outboxPath)) {
1257
- const txt = readFileSync2(this.outboxPath, "utf8");
1851
+ const txt = readFileSync3(this.outboxPath, "utf8");
1258
1852
  for (const line of txt.split("\n")) {
1259
1853
  const s = line.trim();
1260
1854
  if (!s) continue;
@@ -1266,6 +1860,42 @@ var SwarmClient = class _SwarmClient {
1266
1860
  this.nextSeq = this.pending.reduce((m, e) => Math.max(m, e.seq), 0) + 1;
1267
1861
  if (this.autoFlush && this.pending.length) this.scheduleFlush();
1268
1862
  }
1863
+ this.replayGlobalEvents();
1864
+ }
1865
+ // Реплей глобальных (штормовых) событий из сайдкара в зеркало на старте. Идут в
1866
+ // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
1867
+ // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
1868
+ replayGlobalEvents() {
1869
+ if (!existsSync2(this.stormPath)) return;
1870
+ let txt;
1871
+ try {
1872
+ txt = readFileSync3(this.stormPath, "utf8");
1873
+ } catch {
1874
+ return;
1875
+ }
1876
+ for (const line of txt.split("\n")) {
1877
+ const s = line.trim();
1878
+ if (!s) continue;
1879
+ try {
1880
+ const g = JSON.parse(s);
1881
+ this.eventsLog.push({
1882
+ id: this.nextEventId++,
1883
+ run_id: null,
1884
+ kind: g.kind,
1885
+ text: g.text ?? null,
1886
+ actor: g.actor ?? null,
1887
+ at: g.at
1888
+ });
1889
+ } catch {
1890
+ }
1891
+ }
1892
+ }
1893
+ // Дописать глобальное событие в durable-сайдкар (append-only, синхронно — как outbox).
1894
+ persistGlobalEvent(ev) {
1895
+ try {
1896
+ appendFileSync(this.stormPath, JSON.stringify(ev) + "\n");
1897
+ } catch {
1898
+ }
1269
1899
  }
1270
1900
  // ── зеркало: наложить рантайм на durable-прогон ──────────────────────────────
1271
1901
  mat(d) {
@@ -1276,14 +1906,7 @@ var SwarmClient = class _SwarmClient {
1276
1906
  runKeyFor(localId) {
1277
1907
  return this.keyByLocal.get(localId);
1278
1908
  }
1279
- // Локальный id прогона → его cuid в проде (если прогон уже создан/гидрирован).
1280
- // Нужен для отправки ссылки на прогон-героя (hero_run_id → heroId в проде): в
1281
- // зеркале hero_run_id — локальный number, а прод-схеме нужен cuid. TASWM-78.
1282
- prodIdForLocal(localId) {
1283
- const key = this.keyByLocal.get(localId);
1284
- return key ? this.prodIdByKey.get(key) : void 0;
1285
- }
1286
- // Обратно: cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
1909
+ // cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
1287
1910
  // Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
1288
1911
  // прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
1289
1912
  localIdForProdId(prodId) {
@@ -1300,9 +1923,10 @@ var SwarmClient = class _SwarmClient {
1300
1923
  this.scheduleFlush();
1301
1924
  }
1302
1925
  // Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
1303
- // pid/worktree_path (рантайм), superseded_by/resolves_run_id/respawns (нет в схеме
1304
- // тула) и снимок задачи (task_*) в прод НЕ уходят. kind/hero_run_id → role/heroId
1305
- // (иерархия роя, TASWM-78): role — плоский enum; heroId шлём как cuid прогона-героя.
1926
+ // pid/worktree_path (рантайм), respawns (нет в схеме тула) и снимок задачи (task_*,
1927
+ // прод берёт join'ом из Task) в прод НЕ уходят. kind → role (плоский enum). Self-ref
1928
+ // прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
1929
+ // supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
1306
1930
  runFields(patch) {
1307
1931
  const out = {};
1308
1932
  if ("status" in patch && patch.status !== void 0) out.status = patch.status;
@@ -1317,15 +1941,26 @@ var SwarmClient = class _SwarmClient {
1317
1941
  if ("depends_on" in patch && patch.depends_on) out.dependsOn = patch.depends_on;
1318
1942
  if ("decision" in patch) out.decision = patch.decision ? JSON.stringify(patch.decision) : null;
1319
1943
  if ("kind" in patch && patch.kind !== void 0) out.role = patch.kind;
1320
- if ("hero_run_id" in patch) {
1321
- if (patch.hero_run_id == null) out.heroId = null;
1322
- else {
1323
- const hp = this.prodIdForLocal(patch.hero_run_id);
1324
- if (hp) out.heroId = hp;
1325
- }
1326
- }
1944
+ if ("review_verdict" in patch) out.reviewVerdict = patch.review_verdict ?? null;
1945
+ this.setRunRefField(out, "heroId", patch, "hero_run_id");
1946
+ this.setRunRefField(out, "resolvesRunId", patch, "resolves_run_id");
1947
+ this.setRunRefField(out, "supersededById", patch, "superseded_by");
1948
+ this.setRunRefField(out, "reviewsRunId", patch, "reviews_run_id");
1327
1949
  return out;
1328
1950
  }
1951
+ // Положить self-ref прогона в прод-патч: локальный number → отложенная ссылка
1952
+ // {__runRef: runKey} (резолв в cuid на флаше, resolveArgs/materializeRefs). Явный
1953
+ // null → очистка поля. Локальный id без runKey (прогон не в зеркале) — пропускаем.
1954
+ setRunRefField(out, prodKey, patch, localKey) {
1955
+ if (!(localKey in patch)) return;
1956
+ const localId = patch[localKey];
1957
+ if (localId == null) {
1958
+ out[prodKey] = null;
1959
+ return;
1960
+ }
1961
+ const key = this.keyByLocal.get(localId);
1962
+ if (key) out[prodKey] = runRef(key);
1963
+ }
1329
1964
  enqueueRun(run, fields) {
1330
1965
  if (Object.keys(fields).length === 0) return;
1331
1966
  this.enqueue({ t: "run", runKey: run.runKey, taskRef: run.task_slug, fields });
@@ -1358,6 +1993,8 @@ var SwarmClient = class _SwarmClient {
1358
1993
  stop_reason: null,
1359
1994
  respawns: 0,
1360
1995
  resolves_run_id: input.resolves_run_id ?? null,
1996
+ reviews_run_id: input.reviews_run_id ?? null,
1997
+ review_verdict: null,
1361
1998
  task_title: null,
1362
1999
  task_desc: null,
1363
2000
  task_url: null,
@@ -1375,9 +2012,13 @@ var SwarmClient = class _SwarmClient {
1375
2012
  depends_on: d.depends_on,
1376
2013
  decision: d.decision,
1377
2014
  kind: d.kind,
1378
- hero_run_id: d.hero_run_id
2015
+ hero_run_id: d.hero_run_id,
2016
+ resolves_run_id: d.resolves_run_id,
2017
+ // резолвер конфликта → связь на review-прогон
2018
+ reviews_run_id: d.reviews_run_id
2019
+ // ревьюер (авто-ревью, TASWM-148) → связь на review-прогон
1379
2020
  }));
1380
- const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
2021
+ const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
1381
2022
  this.addEvent(id, "spawned", spawnNote);
1382
2023
  return this.mat(d);
1383
2024
  }
@@ -1431,6 +2072,25 @@ var SwarmClient = class _SwarmClient {
1431
2072
  resolverCountFor(reviewRunId) {
1432
2073
  return [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId).length;
1433
2074
  }
2075
+ // failed/stalled ревьюеры (сорвался спавн ДО вердикта) в счёт НЕ идут — см. db.ts:reviewerCountFor.
2076
+ reviewerCountFor(reviewRunId) {
2077
+ return [...this.runs.values()].filter(
2078
+ (r) => r.reviews_run_id === reviewRunId && r.status !== "failed" && r.status !== "stalled"
2079
+ ).length;
2080
+ }
2081
+ // Жёсткий кап ревью-спавнов (TASWM-204) — см. db.ts:reviewerAttemptCountFor.
2082
+ reviewerAttemptCountFor(reviewRunId) {
2083
+ return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).length;
2084
+ }
2085
+ // Последняя попытка ревьюера (TASWM-196) — см. db.ts:lastReviewerAttemptFor.
2086
+ lastReviewerAttemptFor(reviewRunId) {
2087
+ const cands = [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).sort((a, b) => b.id - a.id);
2088
+ return cands.length ? this.mat(cands[0]) : null;
2089
+ }
2090
+ // Живые/ждущие ревьюер-прогоны (TASWM-204) — см. db.ts:activeReviewersFor.
2091
+ activeReviewersFor(reviewRunId) {
2092
+ return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId && !TERMINAL_STATUSES.includes(r.status)).sort((a, b) => b.id - a.id).map((d) => this.mat(d));
2093
+ }
1434
2094
  activeRunForSlug(slug) {
1435
2095
  const cands = [...this.runs.values()].filter((r) => r.task_slug === slug && !SLUG_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
1436
2096
  return cands.length ? this.mat(cands[0]) : null;
@@ -1528,7 +2188,10 @@ var SwarmClient = class _SwarmClient {
1528
2188
  addEvent(run_id, kind, text, actor = null) {
1529
2189
  const ev = { id: this.nextEventId++, run_id, kind, text: text ?? null, actor: actor ?? null, at: stamp() };
1530
2190
  this.eventsLog.push(ev);
1531
- if (run_id == null) return;
2191
+ if (run_id == null) {
2192
+ this.persistGlobalEvent({ kind, text: ev.text, actor: ev.actor, at: ev.at });
2193
+ return;
2194
+ }
1532
2195
  const key = this.runKeyFor(run_id);
1533
2196
  if (key) this.enqueue({ t: "event", runKey: key, kind, text: text ?? null, actor: actor ?? null });
1534
2197
  }
@@ -1561,7 +2224,7 @@ var SwarmClient = class _SwarmClient {
1561
2224
  });
1562
2225
  }
1563
2226
  for (const e of this.eventsLog) {
1564
- if (!MILESTONES.has(e.kind)) continue;
2227
+ if (!isMilestoneKind(e.kind)) continue;
1565
2228
  const global = e.run_id == null;
1566
2229
  const r = runById(e.run_id);
1567
2230
  items.push({
@@ -1618,7 +2281,7 @@ var SwarmClient = class _SwarmClient {
1618
2281
  return this.eventsLog.filter((e) => {
1619
2282
  if (e.kind !== "failed" || e.run_id == null || e.at < cutoff) return false;
1620
2283
  const r = this.runs.get(e.run_id);
1621
- return !!r && r.resolves_run_id == null;
2284
+ return !!r && r.resolves_run_id == null && r.reviews_run_id == null;
1622
2285
  }).length;
1623
2286
  }
1624
2287
  recordStormProbe(probe, ext) {
@@ -1642,6 +2305,100 @@ var SwarmClient = class _SwarmClient {
1642
2305
  if (res?.isError) return;
1643
2306
  const runs = res.structuredContent?.runs ?? [];
1644
2307
  for (const pr of runs) this.hydrateRun(pr);
2308
+ for (const pr of runs) this.linkRefs(pr);
2309
+ }
2310
+ /**
2311
+ * Реконсайл рантайма после рестарта демона (client-режим). pid/worktree_path живут в
2312
+ * ОТДЕЛЬНОЙ Map (машинное — в прод не уходят) и рестарт демона НЕ переживают: гидрация
2313
+ * из прода приносит running/blocked-прогоны с pid=null. Но демон и дроны — РАЗНЫЕ
2314
+ * detached-процессы (launcher: detached+unref), поэтому рестарт демона дронов НЕ убивает
2315
+ * — часть их ещё жива. Сопоставляем каждый running/blocked-прогон зеркала с живым
2316
+ * процессом (ps, injected listProcesses) по детерминированному worktree
2317
+ * (<worktreesDir>/<repo-basename>/<slug>):
2318
+ * • нашёлся живой процесс → восстанавливаем pid+worktree_path (дрон жив — overmind его
2319
+ * НЕ похоронит и не заспавнит второго поверх);
2320
+ * • не нашёлся → дрон не пережил рестарт: восстанавливаем worktree_path, если каталог
2321
+ * цел (для respawn --continue), pid оставляем null. overmind (tickOnce шаг 3) ловит
2322
+ * running/blocked с pid=null как смерть и оживляет/усыновляет/стопорит по общей ветке.
2323
+ * Рантайм пишем ПРЯМО в runtime-Map (в обход updateRun) — чисто локально, без outbox и
2324
+ * без прод-раунд-трипа. Идемпотентно; вызывается один раз на старте после hydrate().
2325
+ * Возвращает счётчики (для лога демона) + reconciled (TASWM-145): false → listProcesses()
2326
+ * бросил (ENFILE и т.п.), реконсайл этого запуска ПРОПУЩЕН целиком (не «confirmed пусто» —
2327
+ * см. коммент в catch ниже). Вызывающий (runDaemon) ОБЯЗАН пропустить startup-sync
2328
+ * (syncCrashedOnStartup) при reconciled=false: та трактует pid==null как «не пережил
2329
+ * рестарт» и безусловно хоронит (stalled/done) — при пропущенном реконсайле это massively
2330
+ * похоронило бы ВСЕХ running/blocked, включая реально живых detached-дронов, тем же
2331
+ * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
2332
+ */
2333
+ reconcileRuntime(opts) {
2334
+ const wtExists = opts.worktreeExists ?? existsSync2;
2335
+ const pidByWorktree = /* @__PURE__ */ new Map();
2336
+ try {
2337
+ for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
2338
+ } catch (e) {
2339
+ logTransientSpawnError("ps (reconcileRuntime)", e);
2340
+ return { alive: 0, dead: 0, reconciled: false };
2341
+ }
2342
+ let alive = 0;
2343
+ let dead = 0;
2344
+ for (const d of this.runs.values()) {
2345
+ if (d.status !== "running" && d.status !== "blocked") continue;
2346
+ if (this.runtime.get(d.id)?.pid != null) continue;
2347
+ const workerWt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
2348
+ let matchWt = null;
2349
+ let matchPid = null;
2350
+ if (d.resolves_run_id == null && d.reviews_run_id == null) {
2351
+ const p = pidByWorktree.get(workerWt);
2352
+ if (p != null) {
2353
+ matchWt = workerWt;
2354
+ matchPid = p;
2355
+ }
2356
+ } else if (d.resolves_run_id != null) {
2357
+ const prefix = `${workerWt}-resolve-`;
2358
+ for (const [wt, p] of pidByWorktree) {
2359
+ if (wt.startsWith(prefix)) {
2360
+ matchWt = wt;
2361
+ matchPid = p;
2362
+ break;
2363
+ }
2364
+ }
2365
+ } else {
2366
+ const reviewed = this.runs.get(d.reviews_run_id);
2367
+ const reviewedWt = reviewed ? droneWorktreePath(opts.worktreesDir, reviewed.repo || this.defaultRepo, reviewed.task_slug) : workerWt;
2368
+ const prefix = `${reviewedWt}-review-`;
2369
+ for (const [wt, p] of pidByWorktree) {
2370
+ if (wt.startsWith(prefix)) {
2371
+ matchWt = wt;
2372
+ matchPid = p;
2373
+ break;
2374
+ }
2375
+ }
2376
+ }
2377
+ if (matchPid != null && matchWt) {
2378
+ this.runtime.set(d.id, { pid: matchPid, worktree_path: matchWt });
2379
+ alive++;
2380
+ } else {
2381
+ const wt = d.resolves_run_id == null && d.reviews_run_id == null && wtExists(workerWt) ? workerWt : null;
2382
+ this.runtime.set(d.id, { pid: null, worktree_path: wt });
2383
+ dead++;
2384
+ }
2385
+ }
2386
+ return { alive, dead, reconciled: true };
2387
+ }
2388
+ // Долинковать self-ref прогона по прод-cuid → локальному id (после того как все
2389
+ // прогоны гидрированы). Идемпотентно: ставит найденное, иначе не трогает.
2390
+ linkRefs(pr) {
2391
+ const localId = this.localIdForProdId(String(pr.id ?? ""));
2392
+ const local = localId === void 0 ? void 0 : this.runs.get(localId);
2393
+ if (!local) return;
2394
+ const hero = pr.heroId ?? null;
2395
+ if (hero) local.hero_run_id = this.localIdForProdId(hero) ?? local.hero_run_id;
2396
+ const res = pr.resolvesRunId ?? null;
2397
+ if (res) local.resolves_run_id = this.localIdForProdId(res) ?? local.resolves_run_id;
2398
+ const sup = pr.supersededById ?? null;
2399
+ if (sup) local.superseded_by = this.localIdForProdId(sup) ?? local.superseded_by;
2400
+ const rev = pr.reviewsRunId ?? null;
2401
+ if (rev) local.reviews_run_id = this.localIdForProdId(rev) ?? local.reviews_run_id;
1645
2402
  }
1646
2403
  /**
1647
2404
  * Авто-интейк (TASWM-100): взять следующую claimable-задачу владельца токена через
@@ -1654,11 +2411,17 @@ var SwarmClient = class _SwarmClient {
1654
2411
  * cuid известен сразу, никакого create-flush. Возвращает локальный Run (queued; overmind
1655
2412
  * заспавнит его следующим тиком) либо null: нет claimable-задачи / прод недоступен / тул
1656
2413
  * отверг — во всех случаях тик продолжается, следующий ретраит.
2414
+ *
2415
+ * TASWM-169: repoFullNames — ЖИВОЙ реестр демона на момент ЭТОГО вызова, а не
2416
+ * последняя heartbeat-декларация (heartbeat — своя кадансность, ~15с; claim — ~60с;
2417
+ * плюс окно до первого успешного heartbeat при рестарте) — свежий аргумент здесь
2418
+ * убирает то окно рассинхрона целиком. Пусто (демон без SWARM_REPOS) → ключ не
2419
+ * шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
1657
2420
  */
1658
2421
  async claim() {
1659
2422
  let res;
1660
2423
  try {
1661
- res = await this.call("swarm_claim_work", {});
2424
+ res = await this.call("swarm_claim_work", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
1662
2425
  } catch {
1663
2426
  return null;
1664
2427
  }
@@ -1668,12 +2431,114 @@ var SwarmClient = class _SwarmClient {
1668
2431
  const local = this.hydrateRun(sc.run);
1669
2432
  return local ? this.mat(local) : null;
1670
2433
  }
2434
+ /**
2435
+ * Живой потолок дронов владельца из Taskless (TASWM-126): читает who_am_i.maxDrones
2436
+ * (личная настройка uiPrefs.swarm.maxDrones, дефолт 3). overmind капит им ВСЕ спавны —
2437
+ * очередь (шаг 1), резолвер (шаг 2), авто-claim (шаг 5) — чтобы активных дронов не было
2438
+ * больше числа из UI (инцидент 15.07: 4-6 дронов разом уронили ноут). who_am_i отдаёт
2439
+ * плоский JSON в text-контенте (не {display,data}). Прод недоступен / нет валидного числа
2440
+ * → null: ридер (makeMaxDronesReader) берёт env-фолбэк.
2441
+ */
2442
+ async ownerMaxDrones() {
2443
+ let res;
2444
+ try {
2445
+ res = await this.call("who_am_i", {});
2446
+ } catch {
2447
+ return null;
2448
+ }
2449
+ if (res?.isError) return null;
2450
+ const obj = jsonFromTextContent(res);
2451
+ const n = obj?.maxDrones;
2452
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
2453
+ }
2454
+ /**
2455
+ * Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
2456
+ * repoFullName (owner/repo из origin — см. git.remoteRepoFullName) — Project.swarmConfig.
2457
+ * verifyMode ("local"/"ci"). Прод недоступен / repo не смэплен на swarm-enabled проект
2458
+ * владельца токена (config=null в ответе) → null: ридер (makeVerifyModeReader) берёт
2459
+ * env-фолбэк SWARM_VERIFY.
2460
+ */
2461
+ async repoVerifyMode(repoFullName) {
2462
+ let res;
2463
+ try {
2464
+ res = await this.call("swarm_repo_config", { repoFullName });
2465
+ } catch {
2466
+ return null;
2467
+ }
2468
+ if (res?.isError) return null;
2469
+ const sc = res.structuredContent ?? {};
2470
+ const v = sc.config?.verifyMode;
2471
+ return v === "local" || v === "ci" ? v : null;
2472
+ }
2473
+ /**
2474
+ * Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
2475
+ * дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
2476
+ * овермайндом» вместо устаревшего «живого» роя). Best-effort, как ownerMaxDrones/claim:
2477
+ * прод недоступен/тул отверг → просто не обновили в этом тике, следующий ретраит —
2478
+ * не роняем тик демона из-за упавшего пульса.
2479
+ *
2480
+ * TASWM-169: заодно декларирует repoFullNames (репо-реестр демона) — та же
2481
+ * кадансность, что и пульс, естественная точка «сказать серверу, что обслуживаю».
2482
+ * Пустой реестр (демон не сконфигурирован на multi-repo/SWARM_REPOS) → ключ не
2483
+ * шлём вовсе (undefined) — прод НЕ трогает прошлую декларацию (см. swarm.ts), не
2484
+ * стирает её ложным «репо больше нет» из-за демона без SWARM_REPOS в env.
2485
+ */
2486
+ async heartbeat() {
2487
+ try {
2488
+ await this.call("swarm_heartbeat", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
2489
+ } catch {
2490
+ }
2491
+ }
2492
+ /**
2493
+ * Встроенный надзор (TASWM-194): нотификация аномалии в инбокс владельца — прод-MCP
2494
+ * `swarm_notify`, ПРЯМОЙ вызов (НЕ outbox): аномалия — «сообщи СЕЙЧАС», не durable-факт,
2495
+ * который обязан пережить рестарт демона в неизменном порядке (как run-апдейты/события) —
2496
+ * задержка на цикл outbox-флаша (обычно доли секунды, но в худшем случае — до
2497
+ * восстановления сети) тут не оправдана.
2498
+ * runId (локальный numeric) резолвим в прод-cuid по той же карте, что flush аутбокса
2499
+ * (runKeyFor→prodIdByKey); прогон ещё не успел долететь до прода (cuid неизвестен) →
2500
+ * шлём без runId (нотификация всё равно уйдёт, просто без SwarmEvent-вехи на прогоне).
2501
+ *
2502
+ * БРОСАЕТ при сбое (транспорт ИЛИ isError:true от тула) — НЕ глотает, в отличие от
2503
+ * heartbeat() выше (code-review TASWM-194): вызывающий (overmind.ts tickOnce) коммитит
2504
+ * AnomalyTracker ТОЛЬКО после успешного return — молча проглоченная ошибка означала бы,
2505
+ * что трекер считает недоставленную нотификацию отправленной и молчит renotifyCooldownMs
2506
+ * (по умолчанию час) без единой реальной попытки. `res?.isError` — тот же паттерн, что
2507
+ * ownerMaxDrones() чуть выше: registerTool ловит throw внутри run() и превращает его в
2508
+ * `{isError:true}`, а не в отклонённый промис — без этой проверки схемная/бизнес-ошибка
2509
+ * тула (напр. TASWM-194 review: body > zod max) молча резолвилась бы успехом.
2510
+ */
2511
+ async notifyAnomaly(signal) {
2512
+ const prodRunId = signal.runId != null ? this.prodIdByKey.get(this.runKeyFor(signal.runId) ?? "") : void 0;
2513
+ const res = await this.call("swarm_notify", {
2514
+ tag: signal.key,
2515
+ title: signal.title,
2516
+ body: signal.body,
2517
+ ...signal.href ? { href: signal.href } : {},
2518
+ ...prodRunId ? { runId: prodRunId } : {}
2519
+ });
2520
+ if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
2521
+ }
2522
+ /**
2523
+ * TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
2524
+ * через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
2525
+ * сервер не знает локальных fs-путей. Транслируем в путь по СВОЕМУ репо-реестру
2526
+ * (см. repoRegistry.ts), т.к. git-операции (addWorktree/droneWorktreePath) ждут
2527
+ * именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
2528
+ * repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
2529
+ * (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
2530
+ */
2531
+ resolveRepo(raw) {
2532
+ if (!raw) return "";
2533
+ return this.repoPathByFullName.get(raw) ?? raw;
2534
+ }
1671
2535
  hydrateRun(pr) {
1672
2536
  const prodId = String(pr.id ?? "");
1673
2537
  if (!prodId) return null;
1674
2538
  const runKey = `p:${prodId}`;
1675
2539
  this.prodIdByKey.set(runKey, prodId);
1676
2540
  let local = [...this.runs.values()].find((r) => r.runKey === runKey);
2541
+ if (!local) local = [...this.runs.values()].find((r) => this.prodIdByKey.get(r.runKey) === prodId);
1677
2542
  if (!local) {
1678
2543
  const id = this.nextRunId++;
1679
2544
  local = {
@@ -1696,6 +2561,8 @@ var SwarmClient = class _SwarmClient {
1696
2561
  stop_reason: null,
1697
2562
  respawns: 0,
1698
2563
  resolves_run_id: null,
2564
+ reviews_run_id: null,
2565
+ review_verdict: null,
1699
2566
  task_title: null,
1700
2567
  task_desc: null,
1701
2568
  task_url: null,
@@ -1707,11 +2574,20 @@ var SwarmClient = class _SwarmClient {
1707
2574
  }
1708
2575
  const slug = pr.taskSlug ?? pr.task?.slug ?? pr.taskId ?? local.task_slug;
1709
2576
  local.task_slug = slug || local.task_slug;
2577
+ local.task_title = pr.taskTitle ?? local.task_title;
2578
+ local.task_desc = pr.taskDesc ?? local.task_desc;
1710
2579
  local.name = pr.droneName ?? local.name;
1711
2580
  local.kind = pr.role ?? local.kind;
1712
2581
  const heroProd = pr.heroId ?? null;
1713
- local.hero_run_id = heroProd ? this.localIdForProdId(heroProd) ?? null : null;
1714
- local.repo = pr.repo ?? "";
2582
+ if (heroProd) local.hero_run_id = this.localIdForProdId(heroProd) ?? local.hero_run_id;
2583
+ const resProd = pr.resolvesRunId ?? null;
2584
+ if (resProd) local.resolves_run_id = this.localIdForProdId(resProd) ?? local.resolves_run_id;
2585
+ const supProd = pr.supersededById ?? null;
2586
+ if (supProd) local.superseded_by = this.localIdForProdId(supProd) ?? local.superseded_by;
2587
+ const revProd = pr.reviewsRunId ?? null;
2588
+ if (revProd) local.reviews_run_id = this.localIdForProdId(revProd) ?? local.reviews_run_id;
2589
+ local.review_verdict = pr.reviewVerdict ?? local.review_verdict;
2590
+ local.repo = this.resolveRepo(pr.repo ?? null);
1715
2591
  local.branch = pr.branch ?? null;
1716
2592
  local.status = pr.status ?? local.status;
1717
2593
  local.area = pr.area ?? null;
@@ -1777,16 +2653,88 @@ var SwarmClient = class _SwarmClient {
1777
2653
  }, this.backoffMs);
1778
2654
  if (typeof this.flushTimer.unref === "function") this.flushTimer.unref();
1779
2655
  }
1780
- resolveArgs(e) {
2656
+ // TASWM-202: create-флаш без прод-id слал taskId=e.taskRef НАПРЯМУЮ — а taskRef это
2657
+ // человеческий слаг (task_slug, напр. «TASWM-171»), не cuid. Резолверы конфликта
2658
+ // рождаются ЛОКАЛЬНО (createRun с uuid-runKey) и попадают в outbox БЕЗ прод-id —
2659
+ // обычные прогоны рождаются в проде (claim) и гидрируются сразу с cuid, эта ветка
2660
+ // их не касается. Прод create-схема ждёт cuid → отбивает слаг → строго
2661
+ // упорядоченный flush стоит на этой голове НАВЕЧНО (ядовитая голова, инцидент
2662
+ // 14.07/18.07, TASWM-201 — общий карантин; здесь — конкретный источник). Резолвим
2663
+ // слаг→cuid через get_task (кэш — taskIdCache) ПЕРЕД тем как слать create.
2664
+ taskIdCache = /* @__PURE__ */ new Map();
2665
+ // task_slug → cuid прода
2666
+ // Create для этого runKey резолвился окончательно неудачно (задача не найдена/
2667
+ // недоступна) — событие/вопросы того же прогона тоже никогда не получат prod-id,
2668
+ // без карантина они бы бросали «нет prod-id» на каждом флаше и СНОВА вставали
2669
+ // ядовитой головой (просто на одну запись ниже по очереди).
2670
+ poisonedRunKeys = /* @__PURE__ */ new Set();
2671
+ // "transient" — прод/сеть недоступны, ответа нет или ошибка НЕ про отсутствие задачи
2672
+ // (offline-стаб CLI, auth-протух, 5xx): НЕ терминально — запись остаётся в outbox и
2673
+ // повторится на следующем флаше (иначе сетевой сбой = потеря прогона). null —
2674
+ // терминально: прод ЯВНО ответил «задачи нет» (task_not_found / not_found в тексте
2675
+ // tool-ошибки) либо успешный ответ без id — только это карантиним.
2676
+ async resolveTaskCuid(taskRef) {
2677
+ const cached = this.taskIdCache.get(taskRef);
2678
+ if (cached) return cached;
2679
+ let res;
2680
+ try {
2681
+ res = await this.call("get_task", { id: taskRef });
2682
+ } catch {
2683
+ return "transient";
2684
+ }
2685
+ if (!res) return "transient";
2686
+ if (res.isError) {
2687
+ const text = (res.content ?? []).map((c) => c.text ?? "").join(" ");
2688
+ return /not_found/i.test(text) ? null : "transient";
2689
+ }
2690
+ const id = res.structuredContent?.task?.id;
2691
+ if (!id) return null;
2692
+ this.taskIdCache.set(taskRef, id);
2693
+ return id;
2694
+ }
2695
+ async resolveArgs(e) {
1781
2696
  if (e.t === "run") {
2697
+ const fields = this.materializeRefs(e.fields);
1782
2698
  const prodId2 = this.prodIdByKey.get(e.runKey);
1783
- return prodId2 ? { tool: "swarm_run_upsert", args: { runId: prodId2, ...e.fields } } : { tool: "swarm_run_upsert", args: { taskId: e.taskRef, ...e.fields } };
2699
+ if (prodId2) return { tool: "swarm_run_upsert", args: { runId: prodId2, ...fields } };
2700
+ const taskId = await this.resolveTaskCuid(e.taskRef);
2701
+ if (taskId === "transient") {
2702
+ throw new Error(`slug\u2192cuid \u0434\u043B\u044F \xAB${e.taskRef}\xBB \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0440\u043E\u0434 \u043C\u043E\u0440\u0433\u043D\u0443\u043B) \u2014 retry`);
2703
+ }
2704
+ if (!taskId) {
2705
+ console.error(
2706
+ `[swarm-client] create-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u043F\u0440\u043E\u0434 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0443 \xAB${e.taskRef}\xBB (runKey=${e.runKey}) \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`
2707
+ );
2708
+ this.poisonedRunKeys.add(e.runKey);
2709
+ return "skip";
2710
+ }
2711
+ return { tool: "swarm_run_upsert", args: { taskId, ...fields } };
2712
+ }
2713
+ if (this.poisonedRunKeys.has(e.runKey)) {
2714
+ console.error(`[swarm-client] ${e.t}-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: create \u043F\u0440\u043E\u0433\u043E\u043D\u0430 runKey=${e.runKey} \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u0435\u0435 \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`);
2715
+ return "skip";
1784
2716
  }
1785
2717
  const prodId = this.prodIdByKey.get(e.runKey);
1786
2718
  if (!prodId) throw new Error(`\u043D\u0435\u0442 prod-id \u0434\u043B\u044F ${e.runKey} \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D \u0432 \u043F\u0440\u043E\u0434\u0435`);
1787
2719
  if (e.t === "event") return { tool: "swarm_event_append", args: { runId: prodId, kind: e.kind, text: e.text, actor: e.actor } };
1788
2720
  return { tool: "swarm_ask", args: { runId: prodId, text: e.text } };
1789
2721
  }
2722
+ // Резолв отложенных self-ref ({__runRef: runKey}) в прод-cuid на флаше. Цель уже
2723
+ // создана в проде (её create-запись с меньшим seq флашится раньше) → cuid известен.
2724
+ // Не резолвится (ещё не создан) — поле выкидываем: лучше не слать, чем слать мусор;
2725
+ // догонит следующий апдейт/реконсайл. Обычные значения (null/строки) — как есть.
2726
+ materializeRefs(fields) {
2727
+ const out = {};
2728
+ for (const [k, v] of Object.entries(fields)) {
2729
+ if (isRunRef(v)) {
2730
+ const cuid = this.prodIdByKey.get(v.__runRef);
2731
+ if (cuid) out[k] = cuid;
2732
+ } else {
2733
+ out[k] = v;
2734
+ }
2735
+ }
2736
+ return out;
2737
+ }
1790
2738
  /** Слить накопленное в прод. Возвращает число успешно отправленных операций.
1791
2739
  * Порядок сохраняется; ошибка/недоступность → стоп, буфер остаётся, retry-backoff. */
1792
2740
  async flush() {
@@ -1798,10 +2746,14 @@ var SwarmClient = class _SwarmClient {
1798
2746
  const e = this.pending[0];
1799
2747
  let call;
1800
2748
  try {
1801
- call = this.resolveArgs(e);
2749
+ call = await this.resolveArgs(e);
1802
2750
  } catch {
1803
2751
  break;
1804
2752
  }
2753
+ if (call === "skip") {
2754
+ this.pending.shift();
2755
+ continue;
2756
+ }
1805
2757
  let res;
1806
2758
  try {
1807
2759
  res = await this.call(call.tool, call.args);
@@ -1815,6 +2767,9 @@ var SwarmClient = class _SwarmClient {
1815
2767
  this.prodIdByKey.set(e.runKey, run.id);
1816
2768
  this.persistMap();
1817
2769
  }
2770
+ } else {
2771
+ const evId = res.structuredContent?.event?.id;
2772
+ if (evId) this.seenProdEventIds.add(evId);
1818
2773
  }
1819
2774
  this.pending.shift();
1820
2775
  sent++;
@@ -1833,13 +2788,13 @@ var SwarmClient = class _SwarmClient {
1833
2788
  }
1834
2789
  persistPending() {
1835
2790
  try {
1836
- writeFileSync(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
2791
+ writeFileSync2(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
1837
2792
  } catch {
1838
2793
  }
1839
2794
  }
1840
2795
  persistMap() {
1841
2796
  try {
1842
- writeFileSync(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
2797
+ writeFileSync2(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
1843
2798
  } catch {
1844
2799
  }
1845
2800
  }
@@ -1863,26 +2818,20 @@ function parseDecision(v) {
1863
2818
  return null;
1864
2819
  }
1865
2820
  }
1866
- function toStamp(v) {
1867
- if (typeof v !== "string" || !v) return null;
1868
- const d = new Date(v);
1869
- if (Number.isNaN(d.getTime())) return v.includes("T") ? v.slice(0, 19).replace("T", " ") : v;
1870
- return stamp(d);
1871
- }
1872
2821
 
1873
2822
  // src/overmind.ts
1874
2823
  init_config();
1875
2824
 
1876
2825
  // src/heartbeat.ts
1877
- import { readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
2826
+ import { readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
1878
2827
  function writeHeartbeat(path, now) {
1879
2828
  const tmp = `${path}.tmp`;
1880
- writeFileSync2(tmp, String(now));
1881
- renameSync(tmp, path);
2829
+ writeFileSync3(tmp, String(now));
2830
+ renameSync2(tmp, path);
1882
2831
  }
1883
2832
  function readHeartbeat(path) {
1884
2833
  try {
1885
- const ms = Number(readFileSync3(path, "utf8").trim());
2834
+ const ms = Number(readFileSync4(path, "utf8").trim());
1886
2835
  return Number.isFinite(ms) ? ms : null;
1887
2836
  } catch {
1888
2837
  return null;
@@ -1894,52 +2843,439 @@ function overmindAlive(path, now, staleMs) {
1894
2843
  return now - ts < staleMs;
1895
2844
  }
1896
2845
 
2846
+ // src/repoRegistry.ts
2847
+ init_config();
2848
+ import { $ } from "execa";
2849
+ function parseRepoFullName(remoteUrl) {
2850
+ const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
2851
+ if (!m) return null;
2852
+ return `${m[1]}/${m[2]}`.toLowerCase();
2853
+ }
2854
+ async function resolveRepoFullName(path) {
2855
+ const res = await $({ reject: false })`git -C ${path} remote get-url origin`;
2856
+ if (res.exitCode !== 0) return null;
2857
+ return parseRepoFullName(res.stdout);
2858
+ }
2859
+ async function buildRepoRegistry(paths) {
2860
+ const resolved = await Promise.all(
2861
+ paths.map(async (raw) => {
2862
+ const path = normalizeRepo(raw);
2863
+ if (!path) return null;
2864
+ const repoFullName = await resolveRepoFullName(path);
2865
+ return repoFullName ? { repoFullName, path } : null;
2866
+ })
2867
+ );
2868
+ return resolved.filter((e) => e !== null);
2869
+ }
2870
+
2871
+ // src/relief.ts
2872
+ var numEnv2 = (name, def) => {
2873
+ const v = Number(process.env[name]);
2874
+ return Number.isFinite(v) && v > 0 ? v : def;
2875
+ };
2876
+ var DEFAULT_RELIEF_CONFIG = {
2877
+ rampWindowMs: numEnv2("SWARM_RELIEF_RAMP_WINDOW_MS", 2e4),
2878
+ pauseWindowMs: numEnv2("SWARM_RELIEF_PAUSE_WINDOW_MS", 3e4),
2879
+ sustainMs: numEnv2("SWARM_RELIEF_SUSTAIN_MS", 15e3)
2880
+ };
2881
+ function resolveReliefConfig(cfg) {
2882
+ return { ...DEFAULT_RELIEF_CONFIG, ...cfg ?? {} };
2883
+ }
2884
+ var RELIEF_PAUSE_REASON = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM, \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 --continue), \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u043F\u043E\u0441\u043B\u0435 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 (TASWM-151)";
2885
+ function rampDue(lastRampAt, now, cfg) {
2886
+ return lastRampAt == null || now - lastRampAt >= cfg.rampWindowMs;
2887
+ }
2888
+ function pauseDue(reliefSince, lastPauseAt, now, cfg) {
2889
+ if (reliefSince == null || now - reliefSince < cfg.sustainMs) return false;
2890
+ return lastPauseAt == null || now - lastPauseAt >= cfg.pauseWindowMs;
2891
+ }
2892
+ function pickPauseCandidate(runs) {
2893
+ const candidates = runs.filter(
2894
+ (r) => r.kind === "drone" && r.status === "running" && r.pid != null && r.resolves_run_id == null && r.reviews_run_id == null
2895
+ );
2896
+ if (!candidates.length) return null;
2897
+ return candidates.reduce((youngest, r) => r.created_at > youngest.created_at ? r : youngest);
2898
+ }
2899
+ function pickResumeCandidate(pausedForRelief) {
2900
+ const candidates = pausedForRelief.filter((r) => r.kind === "drone");
2901
+ if (!candidates.length) return null;
2902
+ return candidates.reduce((oldest, r) => r.updated_at < oldest.updated_at ? r : oldest);
2903
+ }
2904
+
1897
2905
  // src/launcher.ts
1898
2906
  import { spawn } from "child_process";
1899
- import { readdirSync, writeFileSync as writeFileSync3 } from "fs";
2907
+ import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readdirSync, statSync, writeFileSync as writeFileSync4 } from "fs";
1900
2908
  import { homedir as homedir2 } from "os";
1901
2909
  import { join as join2 } from "path";
1902
- function claudeArgs(cfgPath, prompt, continueSession = false) {
1903
- const base2 = ["--dangerously-skip-permissions", "--mcp-config", cfgPath, "-p", prompt];
1904
- return continueSession ? ["--continue", ...base2] : base2;
2910
+ var ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
2911
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = []) {
2912
+ const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
2913
+ const base2 = [
2914
+ "--dangerously-skip-permissions",
2915
+ ...ISOLATION_FLAGS,
2916
+ "--mcp-config",
2917
+ cfgPath,
2918
+ ...pluginFlags,
2919
+ "--output-format",
2920
+ "json",
2921
+ "-p",
2922
+ prompt
2923
+ ];
2924
+ const withModel = model ? ["--model", model, ...base2] : base2;
2925
+ return continueSession ? ["--continue", ...withModel] : withModel;
2926
+ }
2927
+ function roleDir(worktree, kind) {
2928
+ return join2(worktree, ".claude", "roles", kind);
2929
+ }
2930
+ function transcriptDir(worktree, projectsDir) {
2931
+ return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
2932
+ }
2933
+ function sessionTranscriptExists(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
2934
+ try {
2935
+ return readdirSync(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
2936
+ } catch {
2937
+ return false;
2938
+ }
2939
+ }
2940
+ function transcriptMtimeMs(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
2941
+ const dir = transcriptDir(worktree, projectsDir);
2942
+ let files;
2943
+ try {
2944
+ files = readdirSync(dir);
2945
+ } catch {
2946
+ return null;
2947
+ }
2948
+ let latest = null;
2949
+ for (const f of files) {
2950
+ if (!f.endsWith(".jsonl")) continue;
2951
+ try {
2952
+ const mtime2 = statSync(join2(dir, f)).mtimeMs;
2953
+ if (latest === null || mtime2 > latest) latest = mtime2;
2954
+ } catch {
2955
+ }
2956
+ }
2957
+ return latest;
2958
+ }
2959
+ function nextRunLogPath(runLogsDir, runId) {
2960
+ const dir = join2(runLogsDir, String(runId));
2961
+ mkdirSync3(dir, { recursive: true });
2962
+ let files = [];
2963
+ try {
2964
+ files = readdirSync(dir);
2965
+ } catch {
2966
+ }
2967
+ let next = 0;
2968
+ for (const f of files) {
2969
+ const m = /^(\d+)\.json/.exec(f);
2970
+ if (m) next = Math.max(next, Number(m[1]) + 1);
2971
+ }
2972
+ return join2(dir, `${next}.json`);
2973
+ }
2974
+ function openNextRunLog(runLogsDir, runId) {
2975
+ for (let attempt = 0; attempt < 50; attempt++) {
2976
+ try {
2977
+ return openSync(nextRunLogPath(runLogsDir, runId), "wx");
2978
+ } catch (e) {
2979
+ if (e.code !== "EEXIST") throw e;
2980
+ }
2981
+ }
2982
+ throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${runId} (\u0433\u043E\u043D\u043A\u0430?)`);
2983
+ }
2984
+ function makeLauncher(cfg) {
2985
+ return ({ worktree, run, prompt, continueSession }) => {
2986
+ const isCompiled = cfg.overmindEntry.endsWith(".js");
2987
+ if (!isCompiled && !cfg.overmindEntry.endsWith(".ts")) {
2988
+ throw new Error(`overmindEntry \u043D\u0438 .ts, \u043D\u0438 .js: ${cfg.overmindEntry}`);
2989
+ }
2990
+ const mcpConfig = {
2991
+ mcpServers: {
2992
+ overmind: {
2993
+ command: isCompiled ? process.execPath : "bun",
2994
+ args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
2995
+ // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — устойчив к
2996
+ // пересозданию локального стора между спавном и вызовами read_task (droneServer.ts
2997
+ // доверяет ему, а не db.getRun(run.id).task_slug, который может протухнуть/переехать).
2998
+ env: { SWARM_DB: cfg.dbPath, SWARM_TASK_SLUG: run.task_slug }
2999
+ }
3000
+ }
3001
+ };
3002
+ const cfgPath = join2(worktree, ".swarm-mcp.json");
3003
+ writeFileSync4(cfgPath, JSON.stringify(mcpConfig, null, 2));
3004
+ const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
3005
+ const dir = roleDir(worktree, run.kind);
3006
+ const pluginDirs = existsSync3(dir) ? [dir] : [];
3007
+ let stdio = "ignore";
3008
+ let outFd;
3009
+ if (cfg.runLogsDir) {
3010
+ outFd = openNextRunLog(cfg.runLogsDir, run.id);
3011
+ stdio = ["ignore", outFd, "ignore"];
3012
+ }
3013
+ const child = spawn(cfg.claudeBin ?? "claude", claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs), {
3014
+ cwd: worktree,
3015
+ detached: true,
3016
+ stdio
3017
+ });
3018
+ if (outFd !== void 0) closeSync(outFd);
3019
+ child.unref();
3020
+ if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
3021
+ return child.pid;
3022
+ };
3023
+ }
3024
+
3025
+ // src/procCleanup.ts
3026
+ import { execFileSync as execFileSync2 } from "child_process";
3027
+ function sleep(ms) {
3028
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3029
+ }
3030
+ function parseLsofCwd(output) {
3031
+ const rows = [];
3032
+ let pid = null;
3033
+ for (const line of output.split("\n")) {
3034
+ const pMatch = /^p(\d+)$/.exec(line);
3035
+ if (pMatch) {
3036
+ pid = Number(pMatch[1]);
3037
+ } else if (line.startsWith("n") && pid != null) {
3038
+ rows.push({ pid, cwd: line.slice(1) });
3039
+ pid = null;
3040
+ }
3041
+ }
3042
+ return rows;
3043
+ }
3044
+ function cwdUnder(cwd, dir) {
3045
+ return cwd === dir || cwd.startsWith(`${dir}/`);
3046
+ }
3047
+ function pidsWithCwdUnder(dir) {
3048
+ let out;
3049
+ try {
3050
+ out = execFileSync2("lsof", ["-a", "-d", "cwd", "-F", "pn"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
3051
+ } catch (e) {
3052
+ logTransientSpawnError("lsof", e);
3053
+ return [];
3054
+ }
3055
+ return parseLsofCwd(out).filter((r) => cwdUnder(r.cwd, dir));
3056
+ }
3057
+ function pidAlive(pid) {
3058
+ try {
3059
+ process.kill(pid, 0);
3060
+ return true;
3061
+ } catch (e) {
3062
+ return e.code === "EPERM";
3063
+ }
3064
+ }
3065
+ async function killPidsGraceful(pids, opts = {}) {
3066
+ const graceMs = opts.graceMs ?? 3e3;
3067
+ const pollMs = opts.pollMs ?? 150;
3068
+ const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal));
3069
+ const isAlive = opts.isAlive ?? pidAlive;
3070
+ const sleepFn = opts.sleepFn ?? sleep;
3071
+ const signalled = [];
3072
+ for (const pid of pids) {
3073
+ try {
3074
+ kill(pid, "SIGTERM");
3075
+ signalled.push(pid);
3076
+ } catch {
3077
+ }
3078
+ }
3079
+ if (!signalled.length) return signalled;
3080
+ const deadline = Date.now() + graceMs;
3081
+ while (Date.now() < deadline && signalled.some((pid) => isAlive(pid))) {
3082
+ await sleepFn(pollMs);
3083
+ }
3084
+ for (const pid of signalled) {
3085
+ try {
3086
+ if (isAlive(pid)) kill(pid, "SIGKILL");
3087
+ } catch {
3088
+ }
3089
+ }
3090
+ return signalled;
3091
+ }
3092
+ function killProcessGroup(pid, signal) {
3093
+ try {
3094
+ process.kill(-pid, signal);
3095
+ } catch {
3096
+ process.kill(pid, signal);
3097
+ }
3098
+ }
3099
+ function composeProjectPrefix(worktreeDirName) {
3100
+ return worktreeDirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3101
+ }
3102
+ function containerBelongsToPrefix(name, prefix) {
3103
+ return name === prefix || name.startsWith(`${prefix}-`);
3104
+ }
3105
+ function findOrphanContainers(containerNames, runs) {
3106
+ const orphans = [];
3107
+ for (const name of containerNames) {
3108
+ let best = null;
3109
+ for (const r of runs) {
3110
+ if (!containerBelongsToPrefix(name, r.prefix)) continue;
3111
+ if (!best || r.prefix.length > best.prefix.length) best = r;
3112
+ }
3113
+ if (best && !best.active) orphans.push(name);
3114
+ }
3115
+ return orphans;
3116
+ }
3117
+ function dockerContainerNames() {
3118
+ try {
3119
+ return execFileSync2("docker", ["ps", "-a", "--format", "{{.Names}}"], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
3120
+ } catch (e) {
3121
+ logTransientSpawnError("docker ps", e);
3122
+ return [];
3123
+ }
3124
+ }
3125
+ function removeContainers(names) {
3126
+ const removed = [];
3127
+ for (const name of names) {
3128
+ try {
3129
+ execFileSync2("docker", ["rm", "-f", name], { encoding: "utf8" });
3130
+ removed.push(name);
3131
+ } catch (e) {
3132
+ console.warn(`[procCleanup] docker rm -f ${name} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
3133
+ }
3134
+ }
3135
+ return removed;
3136
+ }
3137
+ async function cleanupWorktreeProcesses(worktree, opts = {}) {
3138
+ try {
3139
+ const found = pidsWithCwdUnder(worktree);
3140
+ if (found.length) {
3141
+ const killed = await killPidsGraceful(
3142
+ found.map((f) => f.pid),
3143
+ opts
3144
+ );
3145
+ if (killed.length) console.log(`[procCleanup] worktree ${worktree}: \u043F\u043E\u0433\u0430\u0448\u0435\u043D\u043E \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 ${killed.length} (${killed.join(", ")})`);
3146
+ }
3147
+ } catch (e) {
3148
+ console.error(`[procCleanup] \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
3149
+ }
3150
+ try {
3151
+ const prefix = composeProjectPrefix(worktree.split("/").filter(Boolean).pop() ?? "");
3152
+ if (!prefix) return;
3153
+ const names = dockerContainerNames().filter((n) => containerBelongsToPrefix(n, prefix));
3154
+ if (!names.length) return;
3155
+ const removed = removeContainers(names);
3156
+ if (removed.length) console.log(`[procCleanup] worktree ${worktree}: \u0441\u043D\u0435\u0441\u0435\u043D\u043E docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 ${removed.length} (${removed.join(", ")})`);
3157
+ } catch (e) {
3158
+ console.error(`[procCleanup] \u0441\u043D\u043E\u0441 docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
3159
+ }
3160
+ }
3161
+ async function sweepOrphans(params) {
3162
+ let killedPids = [];
3163
+ try {
3164
+ const found = pidsWithCwdUnder(params.worktreesDir);
3165
+ const orphanPids = found.filter((f) => ![...params.activeWorktreePaths].some((p) => cwdUnder(f.cwd, p))).map((f) => f.pid);
3166
+ if (orphanPids.length) {
3167
+ console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanPids.length} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0446\u0435\u0441\u0441(\u043E\u0432) \u043F\u043E\u0434 ${params.worktreesDir} \u2014 \u0433\u0430\u0448\u0443`, orphanPids);
3168
+ killedPids = await killPidsGraceful(orphanPids, params.killOpts);
3169
+ }
3170
+ } catch (e) {
3171
+ console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
3172
+ }
3173
+ let removedContainers = [];
3174
+ try {
3175
+ const names = dockerContainerNames();
3176
+ const orphanNames = findOrphanContainers(names, params.containerRunPrefixes);
3177
+ if (orphanNames.length) {
3178
+ console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanNames.length} docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440(\u043E\u0432)-\u0441\u0438\u0440\u043E\u0442 \u2014 \u0433\u0430\u0448\u0443`, orphanNames);
3179
+ removedContainers = removeContainers(orphanNames);
3180
+ }
3181
+ } catch (e) {
3182
+ console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
3183
+ }
3184
+ return { killedPids, removedContainers };
3185
+ }
3186
+
3187
+ // src/cost.ts
3188
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2, renameSync as renameSync3 } from "fs";
3189
+ import { join as join3 } from "path";
3190
+
3191
+ // src/pricing.ts
3192
+ var MODEL_PRICING = {
3193
+ "claude-sonnet-5": { inputPerMTok: 3, outputPerMTok: 15 },
3194
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
3195
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
3196
+ };
3197
+ var CACHE_READ_FACTOR = 0.1;
3198
+ var CACHE_WRITE_5M_FACTOR = 1.25;
3199
+ function costFromUsage(usage, model) {
3200
+ const pricing = model ? MODEL_PRICING[model] : void 0;
3201
+ if (!pricing) {
3202
+ return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
3203
+ }
3204
+ const input = usage.input_tokens ?? 0;
3205
+ const output = usage.output_tokens ?? 0;
3206
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
3207
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
3208
+ const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
3209
+ return { costUsd };
1905
3210
  }
1906
- function sessionTranscriptExists(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
1907
- const dir = join2(projectsDir, worktree.replace(/[/.]/g, "-"));
3211
+
3212
+ // src/claudeResult.ts
3213
+ function parseClaudeResultJson(raw) {
3214
+ const trimmed = raw.trim();
3215
+ if (!trimmed) return null;
1908
3216
  try {
1909
- return readdirSync(dir).some((f) => f.endsWith(".jsonl"));
3217
+ const parsed = JSON.parse(trimmed);
3218
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1910
3219
  } catch {
1911
- return false;
3220
+ return null;
1912
3221
  }
1913
3222
  }
1914
- function makeLauncher(cfg) {
1915
- return ({ worktree, run, prompt, continueSession }) => {
1916
- const mcpConfig = {
1917
- mcpServers: {
1918
- overmind: {
1919
- command: "bun",
1920
- args: ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
1921
- env: { SWARM_DB: cfg.dbPath }
1922
- }
1923
- }
1924
- };
1925
- const cfgPath = join2(worktree, ".swarm-mcp.json");
1926
- writeFileSync3(cfgPath, JSON.stringify(mcpConfig, null, 2));
1927
- const child = spawn(cfg.claudeBin ?? "claude", claudeArgs(cfgPath, prompt, continueSession), {
1928
- cwd: worktree,
1929
- detached: true,
1930
- stdio: "ignore"
1931
- });
1932
- child.unref();
1933
- if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
1934
- return child.pid;
1935
- };
3223
+ function launchCostUsd(result, model) {
3224
+ if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
3225
+ return { costUsd: result.total_cost_usd };
3226
+ }
3227
+ if (!result.usage) return { costUsd: 0 };
3228
+ return costFromUsage(result.usage, model);
1936
3229
  }
1937
3230
 
1938
- // src/orchestrator.ts
1939
- import { basename, join as join3 } from "path";
3231
+ // src/cost.ts
3232
+ var UNPROCESSED_LOG_RE = /^\d+\.json$/;
3233
+ function unprocessedLogFiles(dir) {
3234
+ try {
3235
+ return readdirSync2(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
3236
+ } catch {
3237
+ return [];
3238
+ }
3239
+ }
3240
+ function accountRunCost(deps, run) {
3241
+ const dir = join3(deps.runLogsDir, String(run.id));
3242
+ const files = unprocessedLogFiles(dir);
3243
+ if (files.length === 0) return;
3244
+ const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
3245
+ let added = 0;
3246
+ const processed = [];
3247
+ for (const f of files) {
3248
+ const full = join3(dir, f);
3249
+ let raw = null;
3250
+ try {
3251
+ raw = readFileSync5(full, "utf8");
3252
+ } catch {
3253
+ continue;
3254
+ }
3255
+ const result = parseClaudeResultJson(raw);
3256
+ if (!result) {
3257
+ console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: \u043D\u0435 \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u043B\u0441\u044F ${full} (\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u043C\u0435\u0440 \u0434\u043E \u0437\u0430\u043F\u0438\u0441\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430?)`);
3258
+ } else {
3259
+ const est = launchCostUsd(result, model);
3260
+ if (est.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${est.warning}`);
3261
+ added += est.costUsd;
3262
+ }
3263
+ processed.push(full);
3264
+ }
3265
+ if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
3266
+ for (const full of processed) {
3267
+ try {
3268
+ renameSync3(full, `${full}.done`);
3269
+ } catch {
3270
+ }
3271
+ }
3272
+ }
3273
+ function makeCostAccountant(deps) {
3274
+ return (run) => accountRunCost(deps, run);
3275
+ }
1940
3276
 
1941
3277
  // src/git.ts
1942
- import { $ } from "execa";
3278
+ import { $ as $2 } from "execa";
1943
3279
  var WorktreeBusyError = class extends Error {
1944
3280
  constructor(branch, worktreePath) {
1945
3281
  super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
@@ -1951,10 +3287,10 @@ var WorktreeBusyError = class extends Error {
1951
3287
  worktreePath;
1952
3288
  };
1953
3289
  async function revParse(repo, ref) {
1954
- return (await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
3290
+ return (await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
1955
3291
  }
1956
3292
  async function worktreeForBranch(repo, branch) {
1957
- const out = (await $`git -C ${repo} worktree list --porcelain`).stdout;
3293
+ const out = (await $2`git -C ${repo} worktree list --porcelain`).stdout;
1958
3294
  const target = `refs/heads/${branch}`;
1959
3295
  let current = null;
1960
3296
  for (const line of out.split("\n")) {
@@ -1963,22 +3299,29 @@ async function worktreeForBranch(repo, branch) {
1963
3299
  }
1964
3300
  return null;
1965
3301
  }
1966
- async function addWorktree(repo, path, branch, base2 = "HEAD") {
3302
+ async function addWorktree(repo, path, branch, base2) {
1967
3303
  const localSha = await revParse(repo, `refs/heads/${branch}`);
1968
3304
  const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
1969
3305
  if (!localSha && !originSha) {
1970
- await $`git -C ${repo} worktree add -b ${branch} ${path} ${base2}`;
3306
+ const freshBase = base2 ?? await freshWorktreeBase(repo);
3307
+ await $2`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
1971
3308
  return { reused: false };
1972
3309
  }
1973
3310
  const busyAt = await worktreeForBranch(repo, branch);
1974
3311
  if (busyAt) throw new WorktreeBusyError(branch, busyAt);
1975
3312
  if (originSha) {
1976
- await $`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
3313
+ await $2`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
1977
3314
  return { reused: true, from: "origin", sha: originSha };
1978
3315
  }
1979
- await $`git -C ${repo} worktree add ${path} ${branch}`;
3316
+ await $2`git -C ${repo} worktree add ${path} ${branch}`;
1980
3317
  return { reused: true, from: "local" };
1981
3318
  }
3319
+ async function freshWorktreeBase(repo) {
3320
+ const hasOrigin = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).exitCode === 0;
3321
+ if (!hasOrigin) return "HEAD";
3322
+ await $2`git -C ${repo} fetch --quiet origin`;
3323
+ return remoteMainRef(repo);
3324
+ }
1982
3325
  function worktreeSourceMessage(branch, res) {
1983
3326
  if (!res.reused) return null;
1984
3327
  if (res.from === "origin") return `\u0432\u0435\u0442\u043A\u0430 ${branch} \u043F\u0435\u0440\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0430 \u0441 origin/${branch} @ ${res.sha.slice(0, 7)}`;
@@ -1986,49 +3329,56 @@ function worktreeSourceMessage(branch, res) {
1986
3329
  }
1987
3330
  async function setWorktreeIdentity(worktree, name) {
1988
3331
  const local = name.toLowerCase().replace(/\s+/g, "-");
1989
- await $`git -C ${worktree} config extensions.worktreeConfig true`;
1990
- await $`git -C ${worktree} config --worktree user.name ${`${name} (swarm drone)`}`;
1991
- await $`git -C ${worktree} config --worktree user.email ${`${local}@drones.swarm.local`}`;
3332
+ await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3333
+ await $2`git -C ${worktree} config --worktree user.name ${`${name} (swarm drone)`}`;
3334
+ await $2`git -C ${worktree} config --worktree user.email ${`${local}@drones.swarm.local`}`;
1992
3335
  }
1993
3336
  async function removeWorktree(repo, path) {
1994
- const branch = (await $({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
1995
- await $({ reject: false })`git -C ${repo} worktree remove --force ${path}`;
1996
- if (branch) await $({ reject: false })`git -C ${repo} branch -D ${branch}`;
3337
+ const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
3338
+ await cleanupWorktreeProcesses(path);
3339
+ await $2({ reject: false })`git -C ${repo} worktree remove --force ${path}`;
3340
+ if (branch) await $2({ reject: false })`git -C ${repo} branch -D ${branch}`;
1997
3341
  }
1998
3342
  async function worktreeDirty(path) {
1999
- const status = (await $({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
3343
+ const status = (await $2({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
2000
3344
  if (status) return true;
2001
- const unpushed = (await $({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
3345
+ const unpushed = (await $2({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
2002
3346
  return unpushed !== "" && unpushed !== "0";
2003
3347
  }
2004
3348
  async function remoteMainRef(repo) {
2005
- const head = (await $({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
3349
+ const head = (await $2({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
2006
3350
  if (head) return head;
2007
3351
  for (const cand of ["origin/main", "origin/master"]) {
2008
- const ok = await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
3352
+ const ok = await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
2009
3353
  if (ok.exitCode === 0) return cand;
2010
3354
  }
2011
3355
  throw new Error(`\u043D\u0435 \u043D\u0430\u0448\u0451\u043B \u0434\u0435\u0444\u043E\u043B\u0442-\u0432\u0435\u0442\u043A\u0443 origin \u0432 ${repo} (\u043D\u0435\u0442 origin/HEAD, origin/main, origin/master)`);
2012
3356
  }
3357
+ var GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
3358
+ async function remoteRepoFullName(repo) {
3359
+ const url = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).stdout.trim();
3360
+ const m = url.match(GITHUB_REMOTE_RE);
3361
+ return m ? m[1] : null;
3362
+ }
2013
3363
  async function prepareDetachedWorktree(repo, branch, worktree) {
2014
- await $`git -C ${repo} fetch --quiet origin`;
2015
- await $`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
3364
+ await $2`git -C ${repo} fetch --quiet origin`;
3365
+ await $2`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
2016
3366
  }
2017
3367
  async function mergeRef(worktree, ref) {
2018
- const before = (await $`git -C ${worktree} rev-parse HEAD`).stdout.trim();
2019
- const merge = await $({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
3368
+ const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
3369
+ const merge = await $2({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
2020
3370
  if (merge.exitCode === 0) {
2021
- const after = (await $`git -C ${worktree} rev-parse HEAD`).stdout.trim();
3371
+ const after = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
2022
3372
  return { clean: true, conflicts: [], changed: after !== before };
2023
3373
  }
2024
- const u = (await $({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
3374
+ const u = (await $2({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
2025
3375
  return { clean: false, conflicts: u ? u.split("\n") : [], changed: false };
2026
3376
  }
2027
3377
  async function pushHeadTo(worktree, branch) {
2028
- await $`git -C ${worktree} push origin ${`HEAD:${branch}`}`;
3378
+ await $2`git -C ${worktree} push origin ${`HEAD:${branch}`}`;
2029
3379
  }
2030
3380
  async function worktreeLastCommitMs(worktree) {
2031
- const out = (await $({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
3381
+ const out = (await $2({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
2032
3382
  const sec = Number(out);
2033
3383
  return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
2034
3384
  }
@@ -2050,7 +3400,7 @@ async function spawnDrone(deps, run) {
2050
3400
  throw new AreaBusyError(run.area);
2051
3401
  }
2052
3402
  const branch = run.task_slug;
2053
- const worktree = join3(deps.worktreesDir, basename(repo), run.task_slug);
3403
+ const worktree = droneWorktreePath(deps.worktreesDir, repo, run.task_slug);
2054
3404
  const source = await addWorktree(repo, worktree, branch);
2055
3405
  await setWorktreeIdentity(worktree, run.name);
2056
3406
  const sourceMsg = worktreeSourceMessage(branch, source);
@@ -2062,21 +3412,22 @@ async function spawnDrone(deps, run) {
2062
3412
  }
2063
3413
 
2064
3414
  // src/gh.ts
2065
- import { $ as $2 } from "execa";
2066
- var realRunner = async (args) => (await $2`gh ${args}`).stdout;
2067
- var realRepoRunner = async (args, cwd) => (await $2({ cwd })`gh ${args}`).stdout;
3415
+ init_src();
3416
+ import { $ as $3 } from "execa";
3417
+ var realRunner = async (args) => (await $3`gh ${args}`).stdout;
3418
+ var realRepoRunner = async (args, cwd) => (await $3({ cwd })`gh ${args}`).stdout;
2068
3419
  var GH_PR_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+(?:[/?#].*)?$/i;
2069
3420
  function isGithubPrUrl(url) {
2070
3421
  return GH_PR_RE.test(url.trim());
2071
3422
  }
2072
3423
  function softStatus() {
2073
- return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false };
3424
+ return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
2074
3425
  }
2075
3426
  async function prStatus(prUrl, run = realRunner) {
2076
3427
  if (!isGithubPrUrl(prUrl)) return softStatus();
2077
3428
  let out;
2078
3429
  try {
2079
- out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus"]);
3430
+ out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
2080
3431
  } catch {
2081
3432
  return softStatus();
2082
3433
  }
@@ -2095,7 +3446,9 @@ async function prStatus(prUrl, run = realRunner) {
2095
3446
  mergeable,
2096
3447
  mergeStateStatus: j.mergeStateStatus ?? "UNKNOWN",
2097
3448
  // не смержен, открыт и GitHub уверенно говорит CONFLICTING
2098
- conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING"
3449
+ conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
3450
+ isDraft: j.isDraft === true,
3451
+ files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
2099
3452
  };
2100
3453
  }
2101
3454
  async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
@@ -2108,14 +3461,131 @@ async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
2108
3461
  return [];
2109
3462
  }
2110
3463
  }
3464
+ async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
3465
+ if (!branch || !repoDir) return null;
3466
+ try {
3467
+ const out = await run(["pr", "list", "--head", branch, "--state", "merged", "--json", "url", "--limit", "1"], repoDir);
3468
+ const arr = JSON.parse(out);
3469
+ const url = Array.isArray(arr) ? arr[0]?.url : void 0;
3470
+ return typeof url === "string" ? url : null;
3471
+ } catch {
3472
+ return null;
3473
+ }
3474
+ }
2111
3475
  async function closePR(prUrl, run = realRunner) {
2112
3476
  await run(["pr", "close", prUrl, "--comment", "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 (swarm)"]);
2113
3477
  }
3478
+ var realCommentsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json comments`).stdout;
3479
+ var VERDICT_RE = /^\s*\**\s*(APPROVE|REQUEST\s+CHANGES)\b/i;
3480
+ function verdictLabel(verdict) {
3481
+ return verdict === "approve" ? "\u2705 APPROVE" : "\u274C REQUEST CHANGES";
3482
+ }
3483
+ async function latestVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
3484
+ if (!isGithubPrUrl(prUrl)) return null;
3485
+ let out;
3486
+ try {
3487
+ out = await run(prUrl);
3488
+ } catch {
3489
+ return null;
3490
+ }
3491
+ let j;
3492
+ try {
3493
+ j = JSON.parse(out);
3494
+ } catch {
3495
+ return null;
3496
+ }
3497
+ const comments = Array.isArray(j.comments) ? j.comments : [];
3498
+ const sinceMs = sqliteMs(sinceIso);
3499
+ for (let i = comments.length - 1; i >= 0; i--) {
3500
+ const c = comments[i];
3501
+ if (!c?.body || !c.createdAt) continue;
3502
+ if (Number.isFinite(sinceMs) && Date.parse(c.createdAt) < sinceMs) continue;
3503
+ const m = c.body.match(VERDICT_RE);
3504
+ if (!m) continue;
3505
+ const verdict = /^APPROVE/i.test(m[1]) ? "approve" : "changes";
3506
+ return { verdict, createdAt: c.createdAt };
3507
+ }
3508
+ return null;
3509
+ }
3510
+ var realChecksRunner = async (pr) => (await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`).stdout;
3511
+ async function prCheckRows(pr, run) {
3512
+ try {
3513
+ const out = await run(pr);
3514
+ const rows = JSON.parse(out);
3515
+ if (!Array.isArray(rows) || rows.length === 0) return null;
3516
+ return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
3517
+ } catch {
3518
+ return null;
3519
+ }
3520
+ }
3521
+ async function ghChecks(pr, run = realChecksRunner) {
3522
+ const rows = await prCheckRows(pr, run);
3523
+ if (!rows) return null;
3524
+ let pass = 0, fail = 0, pending2 = 0;
3525
+ for (const r of rows) {
3526
+ if (r.bucket === "pass") pass++;
3527
+ else if (r.bucket === "fail" || r.bucket === "cancel") fail++;
3528
+ else if (r.bucket === "pending") pending2++;
3529
+ }
3530
+ const total = rows.length;
3531
+ let bucket, text, tone;
3532
+ if (fail > 0) {
3533
+ bucket = "fail";
3534
+ tone = "fail";
3535
+ text = `\u274C CI \u0443\u043F\u0430\u043B (${fail}/${total})`;
3536
+ } else if (pending2 > 0) {
3537
+ bucket = "pending";
3538
+ tone = "review";
3539
+ text = `\u23F3 CI \u0438\u0434\u0451\u0442 (${pass}/${total})`;
3540
+ } else {
3541
+ bucket = "pass";
3542
+ tone = "merge";
3543
+ text = `\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443`;
3544
+ }
3545
+ return { bucket, text, tone, total, pass, fail, pending: pending2 };
3546
+ }
3547
+ function runIdFromLink(link) {
3548
+ const m = link.match(/\/actions\/runs\/(\d+)/);
3549
+ return m ? m[1] : null;
3550
+ }
3551
+ async function prCiStatus(pr, run = realChecksRunner) {
3552
+ const rows = await prCheckRows(pr, run);
3553
+ if (!rows) return null;
3554
+ const failingRows = rows.filter((r) => r.bucket === "fail" || r.bucket === "cancel");
3555
+ if (failingRows.length > 0) {
3556
+ const failing = failingRows.map((r) => r.name);
3557
+ const failingRunIds = [...new Set(failingRows.map((r) => runIdFromLink(r.link)).filter((id) => id != null))];
3558
+ return { bucket: "fail", failing, failingRunIds };
3559
+ }
3560
+ if (rows.some((r) => r.bucket === "pending")) return { bucket: "pending", failing: [], failingRunIds: [] };
3561
+ return { bucket: "pass", failing: [], failingRunIds: [] };
3562
+ }
3563
+ var realRunLogRunner = async (runId) => (await $3({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`).stdout;
3564
+ var CI_SUMMARY_LOG_CHARS = 4e3;
3565
+ var CI_SUMMARY_MAX_RUNS = 2;
3566
+ async function ciFailureSummary(failing, failingRunIds, logRun = realRunLogRunner) {
3567
+ if (!failing.length) return null;
3568
+ const logs = [];
3569
+ for (const runId of failingRunIds.slice(0, CI_SUMMARY_MAX_RUNS)) {
3570
+ try {
3571
+ const out = await logRun(runId);
3572
+ if (out.trim()) logs.push(out.length > CI_SUMMARY_LOG_CHARS ? `\u2026
3573
+ ${out.slice(-CI_SUMMARY_LOG_CHARS)}` : out);
3574
+ } catch {
3575
+ }
3576
+ }
3577
+ const header = `\u274C CI \u0443\u043F\u0430\u043B: ${failing.join(", ")}`;
3578
+ return logs.length ? `${header}
3579
+
3580
+ ${logs.join("\n---\n")}` : header;
3581
+ }
2114
3582
 
2115
3583
  // src/droneServer.ts
3584
+ init_taskless();
2116
3585
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2117
3586
  import { z } from "zod";
2118
3587
  init_config();
3588
+ init_src();
2119
3589
  function freezeInstruction(slug) {
2120
3590
  return [
2121
3591
  "\u26D4 OVERMIND \u041D\u0415\u0414\u041E\u0421\u0422\u0423\u041F\u0415\u041D \u2014 \u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B. \u041D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0430\u0434 \u0442\u043E\u0431\u043E\u0439 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442: \u043D\u0435\u043A\u043E\u043C\u0443",
@@ -2156,7 +3626,7 @@ function buildDroneServer(db, runId, opts = {}) {
2156
3626
  `overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0443\u043B\u044C\u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u043B > ${Math.round(heartbeatStaleMs / 1e3)}\u0441) \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430`
2157
3627
  );
2158
3628
  }
2159
- const slug = run?.task_slug ?? "<slug>";
3629
+ const slug = opts.expectedTaskSlug ?? run?.task_slug ?? "<slug>";
2160
3630
  return { content: [{ type: "text", text: freezeInstruction(slug) }] };
2161
3631
  };
2162
3632
  server.tool(
@@ -2169,6 +3639,47 @@ function buildDroneServer(db, runId, opts = {}) {
2169
3639
  return { content: [{ type: "text", text: "ok" }] };
2170
3640
  }
2171
3641
  );
3642
+ server.tool(
3643
+ "read_task",
3644
+ "\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C). \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
3645
+ {},
3646
+ async () => {
3647
+ if (!opts.taskless) {
3648
+ return {
3649
+ content: [{ type: "text", text: "Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0441\u043D\u0430\u043F\u0448\u043E\u0442\u0430 \u043D\u0435\u0442, \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435." }],
3650
+ isError: true
3651
+ };
3652
+ }
3653
+ const run = db.getRun(runId);
3654
+ const slug = opts.expectedTaskSlug ?? run?.task_slug;
3655
+ let mismatchNote;
3656
+ if (opts.expectedTaskSlug && !run) {
3657
+ mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0441\u0442\u043E\u0440\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
3658
+ } else if (opts.expectedTaskSlug && run && run.task_slug !== opts.expectedTaskSlug) {
3659
+ mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0441\u0435\u0439\u0447\u0430\u0441 \u0447\u0438\u0441\u043B\u0438\u0442\u0441\u044F \u043A\u0430\u043A \xAB${run.task_slug}\xBB \u2014 \u044D\u0442\u043E \u041D\u0415 \u0442\u0432\u043E\u044F \u0437\u0430\u0434\u0430\u0447\u0430 (\u0441\u0442\u043E\u0440 \u0434\u0435\u043C\u043E\u043D\u0430 \u0440\u0430\u0441\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D, TASWM-211). \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0434\u043E\u0432\u0435\u0440\u0435\u043D\u043D\u044B\u0439 slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
3660
+ }
3661
+ if (!slug) return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D" }], isError: true };
3662
+ const task = await opts.taskless.getTask(slug);
3663
+ if (!task) {
3664
+ const notFoundText = `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 ${slug} \u0438\u0437 Taskless (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435.`;
3665
+ return {
3666
+ content: [{ type: "text", text: mismatchNote ? `${mismatchNote}
3667
+
3668
+ ${notFoundText}` : notFoundText }],
3669
+ isError: true
3670
+ };
3671
+ }
3672
+ const context = await fetchTaskContext(opts.taskless, task);
3673
+ const lines = [
3674
+ `**${task.title}** \xB7 ${task.slug}${task.url ? ` (${task.url})` : ""}`,
3675
+ "",
3676
+ task.description || "(\u043F\u0443\u0441\u0442\u043E)"
3677
+ ];
3678
+ if (context) lines.push("", context);
3679
+ if (mismatchNote) lines.unshift(mismatchNote, "");
3680
+ return { content: [{ type: "text", text: lines.join("\n") }] };
3681
+ }
3682
+ );
2172
3683
  server.tool(
2173
3684
  "ask",
2174
3685
  "\u0417\u0430\u0434\u0430\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u043E\u0442\u0432\u0435\u0442\u0430. \u0415\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F \u2014 \u0434\u0430\u0439 options (2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430, \u0441\u0442\u0430\u043D\u0443\u0442 \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438); \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0432\u044B\u0431\u0435\u0440\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0438\u043B\u0438 \u043D\u0430\u043F\u0438\u0448\u0435\u0442 \u0441\u0432\u043E\u0451. \u041E\u043F\u0443\u0441\u0442\u0438 options \u0434\u043B\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442 \u0441\u0442\u0440\u043E\u043A\u043E\u0439. \u0417\u043E\u0432\u0438, \u043A\u043E\u0433\u0434\u0430 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u2014 \u043D\u0435 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439.",
@@ -2208,15 +3719,46 @@ function buildDroneServer(db, runId, opts = {}) {
2208
3719
  );
2209
3720
  server.tool(
2210
3721
  "done",
2211
- "\u0420\u0430\u0431\u043E\u0442\u0430 \u0441\u0434\u0430\u043D\u0430. \u0421 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C GitHub-PR (github.com/OWNER/REPO/pull/N) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0445\u043E\u0434\u0438\u0442 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u043E\u0439 \u0434\u043E\u043C\u0435\u0440\u0436\u0438\u0442. \u0411\u0435\u0437 \u0441\u0432\u043E\u0435\u0433\u043E PR (\u0440\u0435\u0432\u044C\u044E/meta-\u0437\u0430\u0434\u0430\u0447\u0430) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443; \u043C\u043E\u0436\u0435\u0448\u044C \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443.",
2212
- { pr_url: z.string().describe("\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0439 GitHub-PR; \u0434\u043B\u044F \u0437\u0430\u0434\u0430\u0447 \u0431\u0435\u0437 PR \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044F)") },
2213
- async ({ pr_url }) => {
2214
- if (db.getRun(runId)?.kind === "hero") {
3722
+ "\u0420\u0430\u0431\u043E\u0442\u0430 \u0441\u0434\u0430\u043D\u0430. \u0421 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C GitHub-PR (github.com/OWNER/REPO/pull/N) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0445\u043E\u0434\u0438\u0442 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u043E\u0439 \u0434\u043E\u043C\u0435\u0440\u0436\u0438\u0442. \u0411\u0435\u0437 \u0441\u0432\u043E\u0435\u0433\u043E PR (\u0440\u0435\u0432\u044C\u044E/meta-\u0437\u0430\u0434\u0430\u0447\u0430) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443; \u043C\u043E\u0436\u0435\u0448\u044C \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443. \u0414\u0440\u043E\u043D-\u0420\u0415\u0412\u042C\u042E\u0415\u0420 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u043E\u0431\u044F\u0437\u0430\u043D \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C verdict (\u0438 \u043B\u0443\u0447\u0448\u0435 summary) \u2014 pr_url \u0442\u043E\u0433\u0434\u0430 \u0442\u0430 \u0436\u0435 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 PR.",
3723
+ {
3724
+ pr_url: z.string().describe("\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0439 GitHub-PR; \u0434\u043B\u044F \u0437\u0430\u0434\u0430\u0447 \u0431\u0435\u0437 PR \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044F)"),
3725
+ verdict: z.enum(["approve", "changes"]).optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR \u2014 approve (APPROVE) \u0438\u043B\u0438 changes (REQUEST CHANGES). \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430."),
3726
+ summary: z.string().optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B/\u043D\u0430\u0448\u0451\u043B \u2014 \u0443\u0439\u0434\u0451\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A.")
3727
+ },
3728
+ async ({ pr_url, verdict, summary }) => {
3729
+ const run = db.getRun(runId);
3730
+ if (run?.kind === "hero") {
2215
3731
  return {
2216
3732
  content: [{ type: "text", text: "\u0413\u0435\u0440\u043E\u0439-\u043B\u0435\u0439\u0442\u0435\u043D\u0430\u043D\u0442 \u041D\u0415 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u044D\u043F\u0438\u043A \u0441\u0430\u043C \u0447\u0435\u0440\u0435\u0437 done \u2014 \u044D\u043F\u0438\u043A \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F, \u043A\u043E\u0433\u0434\u0430 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u044B \u0432\u0441\u0435 \u0435\u0433\u043E \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0438. \u0421\u0434\u0435\u043B\u0430\u043B \u0440\u044B\u0432\u043E\u043A (\u0441\u043E\u0437\u0434\u0430\u043B \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0438 / \u0440\u0430\u0437\u0434\u0430\u043B \u043E\u0442\u0432\u0435\u0442\u044B) \u2014 \u0437\u043E\u0432\u0438 rest(summary) \u0438 \u0437\u0430\u043C\u0440\u0438 \u0434\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043E\u0442\u0440\u044F\u0434\u0430." }],
2217
3733
  isError: true
2218
3734
  };
2219
3735
  }
3736
+ if (run?.reviews_run_id != null) {
3737
+ if (isTerminalStatus(run.status)) {
3738
+ return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442 (PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D" }] };
3739
+ }
3740
+ if (verdict !== "approve" && verdict !== "changes") {
3741
+ return {
3742
+ content: [{ type: "text", text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u2014 done() \u0442\u0440\u0435\u0431\u0443\u0435\u0442 verdict: "approve" | "changes" (\u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR).' }],
3743
+ isError: true
3744
+ };
3745
+ }
3746
+ const reviewed = db.getRun(run.reviews_run_id);
3747
+ const verdictText = verdictLabel(verdict);
3748
+ db.updateRun(run.reviews_run_id, { review_verdict: verdict });
3749
+ db.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043E\u0442 ${run.name}: ${verdictText}${summary ? ` \u2014 ${summary}` : ""}`);
3750
+ if (opts.taskless && reviewed) {
3751
+ const note = `${verdictText} \u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${run.name} (\u043F\u0440\u043E\u0433\u043E\u043D #${runId})${summary ? `
3752
+
3753
+ ${summary}` : ""}`;
3754
+ await opts.taskless.addComment(reviewed.task_slug, note).catch(() => {
3755
+ });
3756
+ }
3757
+ const trustedPrUrl = reviewed?.pr_url ?? pr_url;
3758
+ db.updateRun(runId, { status: "review", pr_url: trustedPrUrl });
3759
+ db.addEvent(runId, "pr_opened", trustedPrUrl);
3760
+ return { content: [{ type: "text", text: "\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E" }] };
3761
+ }
2220
3762
  if (isGithubPrUrl(pr_url)) {
2221
3763
  db.updateRun(runId, { status: "review", pr_url });
2222
3764
  db.addEvent(runId, "pr_opened", pr_url);
@@ -2249,6 +3791,7 @@ function buildDroneServer(db, runId, opts = {}) {
2249
3791
  if (!taskless) return errText("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C.");
2250
3792
  const ep = await epic();
2251
3793
  if (!ep) return errText(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0438\u0437 Taskless \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430.`);
3794
+ if (!ep.projectId) return errText(`\u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (\u043D\u0443\u0436\u0435\u043D projectId).`);
2252
3795
  const statusId = await taskless.backlogStatusId();
2253
3796
  if (!statusId) return errText("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0441\u0442\u0430\u0442\u0443\u0441 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 backlog \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430 \u2014 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u044E (\u0440\u0438\u0441\u043A \u043F\u043E\u043F\u0430\u0441\u0442\u044C \u0441\u0440\u0430\u0437\u0443 \u0432 \xAB\u0421\u0434\u0435\u043B\u0430\u0442\u044C\xBB).");
2254
3797
  const footer = `
@@ -2260,7 +3803,9 @@ function buildDroneServer(db, runId, opts = {}) {
2260
3803
  title,
2261
3804
  description: description_md + footer,
2262
3805
  estimateHours: estimate_hours,
2263
- statusId
3806
+ statusId,
3807
+ projectId: ep.projectId
3808
+ // TASWM-138: наследуем проект эпика — иначе сирота не claimable
2264
3809
  });
2265
3810
  if (!created) return errText("Taskless \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043B \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430).");
2266
3811
  db.addEvent(runId, "step", `\u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0430: ${created.slug}`);
@@ -2349,147 +3894,62 @@ async function runDroneServer(runId) {
2349
3894
  const { DB_PATH: DB_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2350
3895
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
2351
3896
  const db = new SwarmDb2(DB_PATH2);
2352
- const taskless = db.getRun(runId)?.kind === "hero" ? await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless() : null;
2353
- const server = buildDroneServer(db, runId, { taskless });
3897
+ const taskless = await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless();
3898
+ const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
3899
+ const server = buildDroneServer(db, runId, { taskless, ...expectedTaskSlug ? { expectedTaskSlug } : {} });
2354
3900
  await server.connect(new StdioServerTransport());
2355
3901
  }
2356
3902
 
2357
3903
  // src/overmind.ts
2358
3904
  init_taskless();
2359
3905
 
2360
- // src/web.ts
2361
- import { randomBytes as randomBytes2 } from "crypto";
2362
- import { basename as basename2 } from "path";
2363
- import { serve } from "@hono/node-server";
2364
- import { $ as $4 } from "execa";
2365
-
2366
- // ../swarm-core/src/timeline.ts
2367
- function buildSegments(created, events, laneEnd) {
2368
- const segs = [];
2369
- let cursor = created;
2370
- let phase = "queued";
2371
- const push = (to) => {
2372
- if (to > cursor) segs.push({ phase, from: cursor, to });
2373
- cursor = to;
2374
- };
2375
- for (const e of events) {
2376
- switch (e.kind) {
2377
- case "step":
2378
- if (phase === "queued") {
2379
- push(e.at);
2380
- phase = "running";
2381
- }
2382
- break;
2383
- case "blocked":
2384
- push(e.at);
2385
- phase = "blocked";
2386
- break;
2387
- case "answered":
2388
- push(e.at);
2389
- phase = "running";
2390
- break;
2391
- case "pr_opened":
2392
- push(e.at);
2393
- phase = "review";
2394
- break;
2395
- case "merged":
2396
- case "done":
2397
- push(e.at);
2398
- phase = "done";
2399
- break;
2400
- case "failed":
2401
- push(e.at);
2402
- phase = "failed";
2403
- break;
2404
- case "stalled":
2405
- push(e.at);
2406
- phase = "stalled";
2407
- break;
3906
+ // src/maxDrones.ts
3907
+ function makeMaxDronesReader(opts) {
3908
+ const ttl = opts.ttlMs ?? 1e4;
3909
+ const now = opts.now ?? Date.now;
3910
+ let cached = null;
3911
+ let cachedAt = -Infinity;
3912
+ return async () => {
3913
+ const t = now();
3914
+ if (cached != null && t - cachedAt < ttl) return cached;
3915
+ const live = await opts.fetch().catch(() => null);
3916
+ if (live != null && Number.isFinite(live) && live > 0) {
3917
+ cached = Math.trunc(live);
3918
+ cachedAt = t;
3919
+ return cached;
2408
3920
  }
2409
- }
2410
- push(laneEnd);
2411
- return segs;
3921
+ return opts.fallback;
3922
+ };
2412
3923
  }
2413
- function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
2414
- switch (status) {
2415
- case "queued": {
2416
- const unmet = deps.filter((d) => !d.met);
2417
- if (unmet.length) {
2418
- const who = unmet.map((d) => d.name ? `${d.slug}\xB7${d.name}` : d.slug).join(", ");
2419
- return { icon: "\u26D4", text: `\u0436\u0434\u0451\u0442 ${who}`, tone: "dep" };
2420
- }
2421
- return { icon: "\u23F8", text: "\u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430 / \u0441\u043F\u0430\u0432\u043D\u0430", tone: "queued" };
2422
- }
2423
- case "running":
2424
- return { icon: "\u25B6", text: lastStep ? `\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442: ${lastStep}` : "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", tone: "run" };
2425
- case "blocked":
2426
- return { icon: "\u2753", text: openQ ? `\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430: ${openQ}` : "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430", tone: "blocked" };
2427
- case "paused":
2428
- return { icon: "\u{1F4A4}", text: "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "blocked" };
2429
- case "review": {
2430
- if (!pr) return { icon: "\u{1F440}", text: "review \u0431\u0435\u0437 PR", tone: "review" };
2431
- if (!ci) return { icon: "\u23F3", text: "CI: \u0436\u0434\u0443 \u0441\u0442\u0430\u0442\u0443\u0441\u2026", tone: "review" };
2432
- if (ci.tone === "merge") return { icon: "\u{1F535}", text: "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430", tone: "merge" };
2433
- return { icon: ci.tone === "fail" ? "\u274C" : "\u23F3", text: ci.text.replace(/^[^ ]+ /, ""), tone: ci.tone };
3924
+
3925
+ // src/verifyMode.ts
3926
+ function makeVerifyModeReader(opts) {
3927
+ const ttl = opts.ttlMs ?? 6e4;
3928
+ const now = opts.now ?? Date.now;
3929
+ const cache = /* @__PURE__ */ new Map();
3930
+ return async (repo) => {
3931
+ const t = now();
3932
+ const hit = cache.get(repo);
3933
+ if (hit && t - hit.at < ttl) return hit.mode;
3934
+ const live = await opts.fetch(repo).catch(() => null);
3935
+ if (live === "local" || live === "ci") {
3936
+ cache.set(repo, { mode: live, at: t });
3937
+ return live;
2434
3938
  }
2435
- case "stalled":
2436
- return { icon: "\u26A0\uFE0F", text: stopReason || (lastStep ? `\u043C\u043E\u043B\u0447\u0438\u0442: ${lastStep}` : "\u043C\u043E\u043B\u0447\u0438\u0442"), tone: "stalled" };
2437
- case "failed":
2438
- return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
2439
- case "stopped":
2440
- return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "queued" };
2441
- case "done":
2442
- return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
2443
- default:
2444
- return { icon: "\u2022", text: status, tone: "queued" };
2445
- }
2446
- }
2447
- function pickHeadline(lanes) {
2448
- const live = lanes.filter((l2) => l2.live && !l2.obsolete);
2449
- const by = (pred) => live.find(pred);
2450
- const fmt = (l2, extra) => `${l2.name} \xB7 ${l2.slug} \u2014 ${extra}`;
2451
- let l = by((x) => x.status === "review" && x.ci?.tone === "fail");
2452
- if (l) return { icon: "\u274C", text: fmt(l, "CI \u0443\u043F\u0430\u043B \u2014 \u0447\u0438\u043D\u0438\u0442\u044C"), tone: "fail" };
2453
- l = by((x) => x.status === "blocked");
2454
- if (l) return { icon: "\u2753", text: fmt(l, "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430"), tone: "blocked" };
2455
- l = by((x) => x.status === "stalled");
2456
- if (l) return { icon: "\u26A0\uFE0F", text: fmt(l, "stalled \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430"), tone: "stalled" };
2457
- l = by((x) => x.status === "review" && x.blocker.tone === "merge");
2458
- if (l) return { icon: "\u{1F535}", text: fmt(l, "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430"), tone: "merge" };
2459
- l = by((x) => x.status === "queued" && x.dependsOn.some((d) => !d.met));
2460
- if (l) {
2461
- const dep = l.dependsOn.find((d) => !d.met);
2462
- return { icon: "\u26D4", text: fmt(l, `\u0436\u0434\u0451\u0442 ${dep.slug}${dep.name ? " \xB7 " + dep.name : ""}`), tone: "dep" };
2463
- }
2464
- l = by((x) => x.status === "review");
2465
- if (l) return { icon: "\u23F3", text: fmt(l, "review \u2014 CI \u0438\u0434\u0451\u0442"), tone: "review" };
2466
- l = by((x) => x.status === "running");
2467
- if (l) return { icon: "\u25B6", text: `${live.filter((x) => x.status === "running").length} \u0434\u0440\u043E\u043D\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435 \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043E\u043A \u043D\u0435\u0442`, tone: "run" };
2468
- return null;
3939
+ return opts.fallback;
3940
+ };
2469
3941
  }
2470
3942
 
2471
- // ../swarm-core/src/status.ts
2472
- var STATUS_META = {
2473
- queued: { pill: "queued", label: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438", color: "--muted-foreground" },
2474
- running: { pill: "run", label: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", color: "--primary" },
2475
- blocked: { pill: "blocked", label: "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430", color: "--warning" },
2476
- paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning" },
2477
- // overmind был недоступен — ждёт пробуждения (TASWM-41)
2478
- review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info" },
2479
- done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success" },
2480
- failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive" },
2481
- stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive" },
2482
- storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning" },
2483
- // умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
2484
- stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground" },
2485
- superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground" }
2486
- };
2487
- var FALLBACK = STATUS_META.queued;
3943
+ // src/web.ts
3944
+ init_src();
3945
+ import { randomBytes as randomBytes2 } from "crypto";
3946
+ import { basename as basename2 } from "path";
3947
+ import { serve } from "@hono/node-server";
2488
3948
 
2489
3949
  // src/diff.ts
2490
- import { statSync } from "fs";
3950
+ import { statSync as statSync2 } from "fs";
2491
3951
  import { join as join6 } from "path";
2492
- import { $ as $3 } from "execa";
3952
+ import { $ as $4 } from "execa";
2493
3953
  var MAX_FILES = 200;
2494
3954
  var MAX_LINES_PER_FILE = 1500;
2495
3955
  var MAX_UNTRACKED = 60;
@@ -2609,12 +4069,12 @@ function truncateFile(f) {
2609
4069
  }
2610
4070
  async function resolveBase(wt) {
2611
4071
  const tryRef = async (ref) => {
2612
- const sha = (await $3({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
4072
+ const sha = (await $4({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
2613
4073
  if (!sha) return null;
2614
- const mb = (await $3({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
4074
+ const mb = (await $4({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
2615
4075
  return mb || null;
2616
4076
  };
2617
- const originHead = (await $3({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
4077
+ const originHead = (await $4({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
2618
4078
  const candidates = [originHead, "origin/main", "origin/master", "main", "master"].filter(Boolean);
2619
4079
  for (const c of candidates) {
2620
4080
  const mb = await tryRef(c);
@@ -2624,7 +4084,7 @@ async function resolveBase(wt) {
2624
4084
  }
2625
4085
  function mtime(path) {
2626
4086
  try {
2627
- return statSync(path).mtimeMs;
4087
+ return statSync2(path).mtimeMs;
2628
4088
  } catch {
2629
4089
  return 0;
2630
4090
  }
@@ -2633,16 +4093,16 @@ async function collectRunDiff(worktree, opts = {}) {
2633
4093
  const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
2634
4094
  const gone = opts.status === "queued" ? "\u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442" : "worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043A\u043E\u0434 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 PR";
2635
4095
  if (!worktree) return stub(gone);
2636
- const isWt = (await $3({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
4096
+ const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
2637
4097
  if (isWt !== "true") return stub(gone);
2638
4098
  const base2 = await resolveBase(worktree);
2639
4099
  const from = base2 ?? "HEAD";
2640
- const trackedTxt = (await $3({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
4100
+ const trackedTxt = (await $4({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
2641
4101
  let files = parseUnifiedDiff(trackedTxt);
2642
- const untrackedRaw = (await $3({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
4102
+ const untrackedRaw = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
2643
4103
  const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
2644
4104
  for (const rel of untracked) {
2645
- const t = (await $3({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
4105
+ const t = (await $4({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
2646
4106
  for (const pf of parseUnifiedDiff(t)) {
2647
4107
  pf.status = "added";
2648
4108
  if (!pf.path || pf.path === "/dev/null") pf.path = rel;
@@ -2671,19 +4131,17 @@ init_oauth();
2671
4131
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2672
4132
  var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
2673
4133
  var TASKLESS_MARK = `<svg viewBox="0 0 60 54" width="19" height="19" fill="none" aria-hidden="true" style="display:block"><g transform="matrix(1.0512,0,0,1.0512,0,-0.021)"><path fill="currentColor" fill-rule="nonzero" d="M46.92,0 L10.24,0 C4.59,0 0,4.59 0,10.24 L0,10.4 C0,16.05 4.59,20.64 10.24,20.64 L16.84,20.64 C19.56,20.64 22.12,19.59 24.05,17.67 L36.06,5.77 C37.25,4.6 38.82,3.95 40.49,3.95 L46.93,3.95 C50.4,3.95 53.22,6.77 53.22,10.24 L53.22,10.4 C53.22,13.87 50.4,16.69 46.93,16.69 L39.82,16.69 C37.05,16.69 34.36,17.83 32.44,19.83 L21.13,31.58 C19.28,33.5 18.27,36.02 18.27,38.68 L18.27,40.94 C18.27,46.59 22.86,51.18 28.51,51.18 C34.16,51.18 38.75,46.59 38.75,40.94 L38.75,20.74 C39.1,20.68 39.46,20.64 39.82,20.64 L46.93,20.64 C52.58,20.64 57.17,16.05 57.17,10.4 L57.17,10.24 C57.17,4.59 52.58,0 46.93,0 L46.92,0 Z M21.26,14.87 C20.07,16.04 18.5,16.69 16.83,16.69 L10.23,16.69 C6.76,16.69 3.94,13.87 3.94,10.4 L3.94,10.24 C3.94,6.77 6.76,3.95 10.23,3.95 L32.27,3.95 L21.25,14.87 L21.26,14.87 Z M34.79,40.94 C34.79,44.41 31.97,47.23 28.5,47.23 C25.03,47.23 22.21,44.41 22.21,40.94 L22.21,38.68 C22.21,37.05 22.83,35.5 23.97,34.32 L34.79,23.07 L34.79,40.94 L34.79,40.94 Z"/></g></svg>`;
2674
- var DAY_MS = 24 * 60 * 60 * 1e3;
2675
- var sqliteMs2 = (ts) => Date.parse(ts.replace(" ", "T") + "Z");
2676
4134
  var sqliteIso = (ts) => {
2677
- const ms = sqliteMs2(ts);
4135
+ const ms = sqliteMs(ts);
2678
4136
  return Number.isNaN(ms) ? ts : new Date(ms).toISOString();
2679
4137
  };
2680
- function roySnapshot(db, now = Date.now()) {
4138
+ function roySnapshot(db, now = Date.now(), overmindLog = []) {
2681
4139
  const all = db.listRuns();
2682
4140
  const c = (s) => all.filter((r) => r.status === s).length;
2683
4141
  const withQ = new Set(db.runIdsWithQuestions());
2684
4142
  const shown = all.filter((r) => {
2685
4143
  if (r.status === "superseded") return false;
2686
- if (r.status === "done" || r.status === "stopped" || r.status === "failed") return sqliteMs2(r.updated_at) >= now - DAY_MS;
4144
+ if (r.status === "done" || r.status === "stopped" || r.status === "failed") return sqliteMs(r.updated_at) >= now - DAY_MS;
2687
4145
  return true;
2688
4146
  });
2689
4147
  return {
@@ -2747,13 +4205,15 @@ function roySnapshot(db, now = Date.now()) {
2747
4205
  active: db.stormActive(),
2748
4206
  since: db.currentStormStartedAt(),
2749
4207
  dead: all.filter((r) => r.status === "storm_dead").length
2750
- }
4208
+ },
4209
+ overmindLog
2751
4210
  };
2752
4211
  }
2753
- var TL_RECENT_TERMINAL_MS = Number(process.env.ROY_WINDOW_H || 5) * 36e5;
2754
- var TL_MAX_TERMINAL_LANES = Number(process.env.ROY_MAX_DONE || 6);
2755
- var TL_MAX_AXIS_MS = Number(process.env.ROY_AXIS_H || 6) * 36e5;
2756
- var TL_TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "stopped", "superseded"]);
4212
+ var {
4213
+ windowMs: TL_RECENT_TERMINAL_MS,
4214
+ maxTerminalLanes: TL_MAX_TERMINAL_LANES,
4215
+ maxAxisMs: TL_MAX_AXIS_MS
4216
+ } = resolveRoyWindows(process.env);
2757
4217
  var ciCache = /* @__PURE__ */ new Map();
2758
4218
  var CI_TTL = 2e4;
2759
4219
  var CI_MOCK = (() => {
@@ -2769,50 +4229,18 @@ function mockCi(bucket) {
2769
4229
  if (b === "pending") return { bucket: "pending", tone: "review", text: "\u23F3 CI \u0438\u0434\u0451\u0442 (1/2)", total: 2, pass: 1, fail: 0, pending: 1 };
2770
4230
  return { bucket: "pass", tone: "merge", text: "\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443", total: 2, pass: 2, fail: 0, pending: 0 };
2771
4231
  }
2772
- async function ghChecks(pr) {
2773
- try {
2774
- const { stdout: out } = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
2775
- const rows = JSON.parse(out);
2776
- if (!Array.isArray(rows) || rows.length === 0) return null;
2777
- let pass = 0, fail = 0, pending2 = 0;
2778
- for (const r of rows) {
2779
- const b = (r.bucket || "").toLowerCase();
2780
- if (b === "pass") pass++;
2781
- else if (b === "fail" || b === "cancel") fail++;
2782
- else if (b === "pending") pending2++;
2783
- }
2784
- const total = rows.length;
2785
- let bucket, text, tone;
2786
- if (fail > 0) {
2787
- bucket = "fail";
2788
- tone = "fail";
2789
- text = `\u274C CI \u0443\u043F\u0430\u043B (${fail}/${total})`;
2790
- } else if (pending2 > 0) {
2791
- bucket = "pending";
2792
- tone = "review";
2793
- text = `\u23F3 CI \u0438\u0434\u0451\u0442 (${pass}/${total})`;
2794
- } else {
2795
- bucket = "pass";
2796
- tone = "merge";
2797
- text = `\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443`;
2798
- }
2799
- return { bucket, text, tone, total, pass, fail, pending: pending2 };
2800
- } catch {
2801
- return null;
2802
- }
2803
- }
2804
4232
  async function refreshCI(prs) {
2805
4233
  await Promise.all(prs.map(async (pr) => {
2806
4234
  const ci = await ghChecks(pr);
2807
4235
  ciCache.set(pr, { ci, at: Date.now() });
2808
4236
  }));
2809
4237
  }
2810
- function timelineState(db, now = Date.now()) {
4238
+ function timelineState(db, now = Date.now(), overmindLog = []) {
2811
4239
  const runs = db.listRuns().slice().sort((a, b) => a.id - b.id);
2812
4240
  const doneNameBySlug = /* @__PURE__ */ new Map();
2813
4241
  const activeBySlug = /* @__PURE__ */ new Map();
2814
4242
  for (const r of runs) {
2815
- if (!TL_TERMINAL.has(r.status)) activeBySlug.set(r.task_slug, { id: r.id, name: r.name });
4243
+ if (!isTerminalStatus(r.status)) activeBySlug.set(r.task_slug, { id: r.id, name: r.name });
2816
4244
  if (r.status === "done") doneNameBySlug.set(r.task_slug, r.name);
2817
4245
  }
2818
4246
  const openQ = /* @__PURE__ */ new Map();
@@ -2822,9 +4250,9 @@ function timelineState(db, now = Date.now()) {
2822
4250
  const lanes = [];
2823
4251
  const reviewPRs = [];
2824
4252
  for (const r of runs) {
2825
- const live = !TL_TERMINAL.has(r.status);
4253
+ const live = !isTerminalStatus(r.status);
2826
4254
  const recencyGated = !live || r.status === "stalled";
2827
- if (recencyGated && sqliteMs2(r.updated_at) < cutoff) continue;
4255
+ if (recencyGated && sqliteMs(r.updated_at) < cutoff) continue;
2828
4256
  const events = db.events(r.id).map((e) => ({ kind: e.kind, text: e.text ?? null, at: sqliteIso(e.at) }));
2829
4257
  const steps = events.filter((e) => e.kind === "step" && e.text).map((e) => ({ at: e.at, text: e.text }));
2830
4258
  const lastStep = steps.length ? steps[steps.length - 1].text : null;
@@ -2864,7 +4292,11 @@ function timelineState(db, now = Date.now()) {
2864
4292
  ci,
2865
4293
  pr: r.pr_url,
2866
4294
  dependsOn: deps,
2867
- cost: r.cost_usd || 0
4295
+ cost: r.cost_usd || 0,
4296
+ role: r.kind,
4297
+ heroRunId: r.hero_run_id,
4298
+ reviewVerdict: r.review_verdict,
4299
+ model: r.model
2868
4300
  });
2869
4301
  }
2870
4302
  const liveLanes = lanes.filter((l) => l.live);
@@ -2894,7 +4326,7 @@ function timelineState(db, now = Date.now()) {
2894
4326
  const headline = pickHeadline(shown);
2895
4327
  const stale = [...new Set(reviewPRs)].filter((pr) => now - (ciCache.get(pr)?.at ?? 0) > CI_TTL);
2896
4328
  if (stale.length) void refreshCI(stale);
2897
- return { now: nowIso, axisStart, axisEnd: nowIso, counters: c, headline, lanes: shown };
4329
+ return { now: nowIso, axisStart, axisEnd: nowIso, counters: c, headline, lanes: shown, overmindLog };
2898
4330
  }
2899
4331
  var CSS = `
2900
4332
  :root {
@@ -3032,7 +4464,7 @@ main { max-width: 1080px; margin: 0 auto; width: 100%; }
3032
4464
  .pill .sd { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
3033
4465
  .pill.run { color: var(--primary); background: var(--primary-soft); } .pill.run .sd { animation: pulse 1.8s infinite; }
3034
4466
  .pill.review { color: var(--info); background: color-mix(in oklch, var(--info) 14%, transparent); }
3035
- .pill.blocked { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
4467
+ .pill.blocked, .pill.paused, .pill.storm { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
3036
4468
  .pill.done { color: var(--success); background: color-mix(in oklch, var(--success) 13%, transparent); }
3037
4469
  .pill.stalled, .pill.failed { color: var(--destructive); background: color-mix(in oklch, var(--destructive) 14%, transparent); }
3038
4470
  .pill.stopped, .pill.queued, .pill.superseded { color: var(--muted-foreground); background: var(--secondary); }
@@ -3325,9 +4757,11 @@ var GLYPH = {
3325
4757
  hex:'<path d="M12 2l8 4.6v9.2L12 21l-8-5.2V6.6L12 2z" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round"/><circle cx="12" cy="12" r="2" fill="currentColor"/>'
3326
4758
  };
3327
4759
  var GK = ['claw','spore','eye','hex'];
3328
- var ST = { running:['run','\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442','--primary'], review:['review','\u043D\u0430 \u0440\u0435\u0432\u044C\u044E','--info'], blocked:['blocked','\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430','--warning'], paused:['blocked','\u0437\u0430\u043C\u0435\u0440 \u2014 \u0436\u0434\u0451\u0442 overmind','--warning'], storm_dead:['blocked','\u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)','--warning'], done:['done','\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E','--success'], stalled:['stalled','\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441','--destructive'], failed:['failed','\u0443\u043F\u0430\u043B','--destructive'], stopped:['stopped','\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D','--muted-foreground'], queued:['queued','\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438','--muted-foreground'], superseded:['superseded','\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D','--muted-foreground'] };
4760
+ /* \u041A\u0430\u0440\u0442\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u2192[pill,label,color] \u0438 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u2014 \u0438\u0437 swarm-core (statusTuples/
4761
+ TERMINAL_STATUSES), \u0430 \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434-\u043A\u043E\u043F\u0438\u044F (\u043E\u043D\u0430 \u0443\u0436\u0435 \u0440\u0430\u0441\u0445\u043E\u0434\u0438\u043B\u0430\u0441\u044C: paused/storm_dead). */
4762
+ var ST = ${jsonForScript(statusTuples())};
3329
4763
  var ORDER = { running:0, blocked:1, paused:2, storm_dead:3, review:4, stalled:5, queued:6, stopped:7, done:8, failed:9, superseded:10 };
3330
- var TERM = { done:1, stopped:1, failed:1, superseded:1 };
4764
+ var TERM = ${jsonForScript(Object.fromEntries(TERMINAL_STATUSES.map((s) => [s, 1])))};
3331
4765
  var EVL = { spawned:'\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D', step:'', blocked:'\u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443', answered:'\u043F\u043E\u043B\u0443\u0447\u0435\u043D \u043E\u0442\u0432\u0435\u0442', pr_opened:'PR \u043E\u0442\u043A\u0440\u044B\u0442', merged:'PR \u0441\u043C\u0435\u0440\u0436\u0451\u043D, \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430', done:'\u0433\u043E\u0442\u043E\u0432\u043E', failed:'\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B', stalled:'\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431', respawned:'\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435', paused:'\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', resumed:'overmind \u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u2014 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B', rescued:'worktree \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438', conflict:'\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main (merge-main-in)', died:'\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440', storm_start:'\u26A1 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442', storm_end:'\u0441\u0432\u044F\u0437\u044C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F', storm_dead:'\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)', stopped:'\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D', superseded:'\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D' };
3332
4766
  var MDOT = { merged:'--success', done:'--success', pr_opened:'--info', spawned:'--muted-foreground', stalled:'--destructive', failed:'--destructive', respawned:'--warning', paused:'--warning', resumed:'--success', rescued:'--destructive', conflict:'--warning', died:'--destructive', storm_start:'--destructive', storm_end:'--success', storm_dead:'--warning', stopped:'--muted-foreground', superseded:'--muted-foreground' };
3333
4767
 
@@ -3360,7 +4794,7 @@ function cardStep(d){
3360
4794
  if(d.status==='stalled') return d.reason || '\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431';
3361
4795
  if(d.status==='stopped') return d.stopReason || d.reason || '\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D';
3362
4796
  if(d.status==='storm_dead') return '\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 (Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442), worktree \u0446\u0435\u043B, \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043E\u0442\u0431\u043E\u044F';
3363
- if(d.status==='paused') return '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430';
4797
+ if(d.status==='paused') return d.stopReason || '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430'; // TASWM-151: \u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0437\u0430\u0434\u0430\u0451\u0442 \u0441\u0432\u043E\u0439 stopReason
3364
4798
  if(d.status==='review') { var n=prNum(d.pr); return 'PR'+(n?(' #'+n):'')+' \u043D\u0430 \u0440\u0435\u0432\u044C\u044E'; }
3365
4799
  if(d.status==='done') return '\u0441\u043C\u0435\u0440\u0436\u0435\u043D';
3366
4800
  if(d.status==='queued') return (d.waiting&&d.waiting.length) ? '\u0436\u0434\u0451\u0442: '+d.waiting.join(', ') : '\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430';
@@ -3829,14 +5263,15 @@ async function runDiff(db, id, now = Date.now()) {
3829
5263
  diffCache.set(id, { at: now, payload });
3830
5264
  return payload;
3831
5265
  }
3832
- function eventsStream(db) {
5266
+ var readNarratorLog = (log) => log?.() ?? [];
5267
+ function eventsStream(db, narratorLog) {
3833
5268
  let iv;
3834
5269
  let last = "";
3835
5270
  const stream = new ReadableStream({
3836
5271
  start(controller) {
3837
5272
  const enc = new TextEncoder();
3838
5273
  const send = () => {
3839
- const snap = JSON.stringify(roySnapshot(db));
5274
+ const snap = JSON.stringify(roySnapshot(db, Date.now(), readNarratorLog(narratorLog)));
3840
5275
  if (snap === last) return;
3841
5276
  last = snap;
3842
5277
  try {
@@ -3870,11 +5305,11 @@ function makeHandler(deps) {
3870
5305
  const p = url.pathname;
3871
5306
  if (p === "/") {
3872
5307
  const store = auth();
3873
- return html(store ? renderDashboard(store.email, roySnapshot(deps.db), taskUrlBase) : renderLogin());
5308
+ return html(store ? renderDashboard(store.email, roySnapshot(deps.db, Date.now(), readNarratorLog(deps.narratorLog)), taskUrlBase) : renderLogin());
3874
5309
  }
3875
- if (p === "/events") return eventsStream(deps.db);
3876
- if (p === "/state") return Response.json(roySnapshot(deps.db));
3877
- if (p === "/timeline") return Response.json(timelineState(deps.db));
5310
+ if (p === "/events") return eventsStream(deps.db, deps.narratorLog);
5311
+ if (p === "/state") return Response.json(roySnapshot(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
5312
+ if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
3878
5313
  const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
3879
5314
  if (diffMatch) {
3880
5315
  if (!auth()) return unauthorized();
@@ -3952,9 +5387,31 @@ function makeHandler(deps) {
3952
5387
  function startWeb(deps, port = WEB_PORT) {
3953
5388
  const handle = makeHandler(deps);
3954
5389
  const server = serve({ fetch: handle, port, hostname: "127.0.0.1" });
5390
+ server.on("error", (err) => {
5391
+ const e = err;
5392
+ if (e.code === "EADDRINUSE") {
5393
+ console.error(`\u043F\u043E\u0440\u0442 ${port} \u0437\u0430\u043D\u044F\u0442 \u2014 \u043F\u043E\u0445\u043E\u0436\u0435, overmind \u0443\u0436\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D (\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${port}).`);
5394
+ } else {
5395
+ console.error(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C: ${e.message}`);
5396
+ }
5397
+ process.exit(1);
5398
+ });
3955
5399
  return { port, server };
3956
5400
  }
3957
5401
 
5402
+ // src/narration.ts
5403
+ var RING_LIMIT = 20;
5404
+ function makeOvermindNarrator() {
5405
+ let ring = [];
5406
+ return {
5407
+ narrate(text) {
5408
+ ring.push({ at: (/* @__PURE__ */ new Date()).toISOString(), text });
5409
+ if (ring.length > RING_LIMIT) ring = ring.slice(-RING_LIMIT);
5410
+ },
5411
+ log: () => ring.slice()
5412
+ };
5413
+ }
5414
+
3958
5415
  // src/probe.ts
3959
5416
  import { spawn as spawn2 } from "child_process";
3960
5417
  function anthropicHealthy(opts = {}) {
@@ -3972,7 +5429,7 @@ function anthropicHealthy(opts = {}) {
3972
5429
  }
3973
5430
  resolve3(ok);
3974
5431
  };
3975
- const child = spawn2(bin, ["-p", "ok", "--max-turns", "1"], { stdio: "ignore" });
5432
+ const child = spawn2(bin, [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"], { stdio: "ignore" });
3976
5433
  const timer = setTimeout(() => finish(false), timeoutMs);
3977
5434
  child.on("error", () => finish(false));
3978
5435
  child.on("exit", (code) => finish(code === 0));
@@ -3998,8 +5455,161 @@ async function fetchExternalStorm(url, opts = {}) {
3998
5455
  }
3999
5456
  }
4000
5457
 
5458
+ // src/anomalies.ts
5459
+ init_src();
5460
+ var AnomalyTracker = class {
5461
+ constructor(renotifyCooldownMs) {
5462
+ this.renotifyCooldownMs = renotifyCooldownMs;
5463
+ }
5464
+ renotifyCooldownMs;
5465
+ since = /* @__PURE__ */ new Map();
5466
+ lastFiredAt = /* @__PURE__ */ new Map();
5467
+ /** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
5468
+ eligible(now, candidates) {
5469
+ const result = [];
5470
+ const seen = /* @__PURE__ */ new Set();
5471
+ for (const c of candidates) {
5472
+ seen.add(c.key);
5473
+ if (!this.since.has(c.key)) this.since.set(c.key, now);
5474
+ const startedAt = this.since.get(c.key);
5475
+ if (now - startedAt < c.minDurationMs) continue;
5476
+ const lastFired = this.lastFiredAt.get(c.key);
5477
+ if (lastFired != null && now - lastFired < this.renotifyCooldownMs) continue;
5478
+ result.push(c);
5479
+ }
5480
+ for (const k of [...this.since.keys()]) if (!seen.has(k)) this.since.delete(k);
5481
+ for (const k of [...this.lastFiredAt.keys()]) if (!seen.has(k)) this.lastFiredAt.delete(k);
5482
+ return result;
5483
+ }
5484
+ /** Доставка signal с этим key успешно ушла в этот момент — начинает кулдаун повтора. */
5485
+ commit(key, now) {
5486
+ this.lastFiredAt.set(key, now);
5487
+ }
5488
+ };
5489
+ var REVIEW_LIKE_STATUS = "review";
5490
+ var MAX_BODY_CHARS = 1800;
5491
+ function ageMs(now, iso) {
5492
+ return now - sqliteMs(iso);
5493
+ }
5494
+ function runLabel(run) {
5495
+ return run.name || `#${run.id}`;
5496
+ }
5497
+ function hrefOf(url) {
5498
+ return url ? { href: url } : {};
5499
+ }
5500
+ function joinCapped(labels) {
5501
+ let joined = labels.join(", ");
5502
+ if (joined.length <= MAX_BODY_CHARS) return joined;
5503
+ let cut = 0;
5504
+ let len = 0;
5505
+ for (; cut < labels.length; cut++) {
5506
+ len += labels[cut].length + 2;
5507
+ if (len > MAX_BODY_CHARS) break;
5508
+ }
5509
+ joined = labels.slice(0, Math.max(cut, 1)).join(", ");
5510
+ return `${joined} \u0438 \u0435\u0449\u0451 ${labels.length - Math.max(cut, 1)}`;
5511
+ }
5512
+ function buildAnomalyCandidates(db, now, thresholds, opts) {
5513
+ const candidates = [];
5514
+ const allRuns = db.listRuns();
5515
+ const recentFailed = allRuns.filter(
5516
+ (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs
5517
+ );
5518
+ if (recentFailed.length >= thresholds.failedSpikeCount) {
5519
+ candidates.push({
5520
+ key: "failed_spike",
5521
+ minDurationMs: 0,
5522
+ build: () => ({
5523
+ title: `\u0420\u043E\u0439: ${recentFailed.length} \u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0437\u0430 ${Math.round(thresholds.failedSpikeWindowMs / 6e4)} \u043C\u0438\u043D`,
5524
+ body: `${joinCapped(recentFailed.map(runLabel))} \u2014 \u0440\u043E\u0441\u0442 failed \u0437\u0430 \u043E\u043A\u043D\u043E, \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u0441\u0438\u0441\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0443\u044E \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443, \u0430 \u043D\u0435 \u0435\u0434\u0438\u043D\u0438\u0447\u043D\u044B\u0439 \u0441\u0431\u043E\u0439.`
5525
+ })
5526
+ });
5527
+ }
5528
+ for (const run of allRuns) {
5529
+ if (run.status !== "stalled") continue;
5530
+ candidates.push({
5531
+ key: `run_stalled:${run.id}`,
5532
+ minDurationMs: 0,
5533
+ // само по себе попадание в stalled УЖЕ прошло свой STALL_MS/RESPAWN_LIMIT
5534
+ build: () => ({
5535
+ runId: run.id,
5536
+ title: `\u0414\u0440\u043E\u043D ${runLabel(run)} \u0437\u0430\u0432\u0438\u0441`,
5537
+ body: `\u041F\u0440\u043E\u0433\u043E\u043D ${run.task_slug} \u043F\u043E\u043C\u0435\u0447\u0435\u043D stalled \u2014 \u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`,
5538
+ ...hrefOf(run.task_url)
5539
+ })
5540
+ });
5541
+ }
5542
+ const queuedCount = allRuns.filter((r) => r.status === "queued").length;
5543
+ const activeDroneCount = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
5544
+ if (queuedCount > 0 && activeDroneCount === 0) {
5545
+ candidates.push({
5546
+ key: "queue_stalled",
5547
+ minDurationMs: thresholds.queueStalledMs,
5548
+ build: () => ({
5549
+ title: `\u0420\u043E\u0439 \u0441\u0442\u043E\u0438\u0442: \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
5550
+ body: `${queuedCount} \u043F\u0440\u043E\u0433\u043E\u043D(\u043E\u0432) \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0440\u043E\u043D\u0430 \u2265${Math.round(thresholds.queueStalledMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u043E\u0445\u043E\u0436\u0435, \u0433\u0435\u0439\u0442 (\u0440\u0435\u0441\u0443\u0440\u0441\u044B/maxDrones/\u0448\u0442\u043E\u0440\u043C) \u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043D\u0442\u0435\u0439\u043A \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043E\u043B\u0433\u043E.`
5551
+ })
5552
+ });
5553
+ }
5554
+ for (const run of allRuns) {
5555
+ if (run.status !== REVIEW_LIKE_STATUS) continue;
5556
+ if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
5557
+ if (!run.pr_url) continue;
5558
+ if (run.review_verdict === "approve") {
5559
+ candidates.push({
5560
+ key: `pr_stuck_merge:${run.id}`,
5561
+ minDurationMs: thresholds.prStuckMergeMs,
5562
+ build: () => ({
5563
+ runId: run.id,
5564
+ title: `PR ${runLabel(run)} \u043E\u0434\u043E\u0431\u0440\u0435\u043D, \u043D\u043E \u043D\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D`,
5565
+ body: `${run.task_slug}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B APPROVE \u2265${Math.round(thresholds.prStuckMergeMs / 6e4)} \u043C\u0438\u043D \u043D\u0430\u0437\u0430\u0434, PR \u0432\u0441\u0451 \u0435\u0449\u0451 \u043E\u0442\u043A\u0440\u044B\u0442 \u2014 ${run.pr_url}.`,
5566
+ ...hrefOf(run.task_url)
5567
+ })
5568
+ });
5569
+ } else if (opts.autoReviewEnabled && run.review_verdict == null && db.reviewerCountFor(run.id) === 0) {
5570
+ candidates.push({
5571
+ key: `pr_no_reviewer:${run.id}`,
5572
+ minDurationMs: thresholds.prNoReviewerMs,
5573
+ build: () => ({
5574
+ runId: run.id,
5575
+ title: `PR ${runLabel(run)} \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430`,
5576
+ body: `${run.task_slug}: PR \u043E\u0442\u043A\u0440\u044B\u0442 \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0442\u0430\u043A \u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (SWARM_REVIEW_CAP/maxDrones/cutoff?) \u2014 ${run.pr_url}.`,
5577
+ ...hrefOf(run.task_url)
5578
+ })
5579
+ });
5580
+ }
5581
+ }
5582
+ const runsById = new Map(allRuns.map((r) => [r.id, r]));
5583
+ for (const q of db.openQuestions()) {
5584
+ const run = runsById.get(q.run_id);
5585
+ candidates.push({
5586
+ key: `question_unanswered:${q.id}`,
5587
+ minDurationMs: thresholds.questionUnansweredMs,
5588
+ build: () => ({
5589
+ runId: q.run_id,
5590
+ title: `${run ? runLabel(run) : `\u043F\u0440\u043E\u0433\u043E\u043D #${q.run_id}`} \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430`,
5591
+ body: `\u0412\u043E\u043F\u0440\u043E\u0441 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2265${Math.round(thresholds.questionUnansweredMs / 6e4)} \u043C\u0438\u043D: \xAB${q.text}\xBB.`,
5592
+ ...hrefOf(run?.task_url)
5593
+ })
5594
+ });
5595
+ }
5596
+ if (opts.outboxPendingCount != null && opts.outboxPendingCount > 0) {
5597
+ const pending2 = opts.outboxPendingCount;
5598
+ candidates.push({
5599
+ key: "outbox_stuck",
5600
+ minDurationMs: thresholds.outboxStuckMs,
5601
+ build: () => ({
5602
+ title: `Outbox \u0440\u043E\u044F \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
5603
+ body: `${pending2} \u043E\u043F\u0435\u0440\u0430\u0446\u0438(\u0439) \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E \u0434\u043E \u043F\u0440\u043E\u0434\u0430 \u2265${Math.round(thresholds.outboxStuckMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u0440\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0434\u043E\u043B\u044C\u0448\u0435 \u043E\u0431\u044B\u0447\u043D\u043E\u0433\u043E \u0441\u0431\u043E\u044F.`
5604
+ })
5605
+ });
5606
+ }
5607
+ return candidates;
5608
+ }
5609
+
4001
5610
  // src/overmind.ts
4002
5611
  init_storm();
5612
+ init_src();
4003
5613
  var THIS_FILE = fileURLToPath(import.meta.url);
4004
5614
  var OVERMIND_ENTRY = THIS_FILE;
4005
5615
  var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
@@ -4007,38 +5617,199 @@ var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
4007
5617
  var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
4008
5618
  var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
4009
5619
  var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
4010
- function pidAlive(pid) {
5620
+ var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
5621
+ var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
5622
+ var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
5623
+ var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
5624
+ var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
5625
+ var CI_CHECK_COOLDOWN_MS = Number(process.env.SWARM_CI_CHECK_COOLDOWN_MS) || 60 * 1e3;
5626
+ var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u0441\u044C";
5627
+ function psLines(extraArgs) {
4011
5628
  try {
4012
- process.kill(pid, 0);
4013
- return true;
5629
+ return execFileSync3("ps", ["-A", "-ww", ...extraArgs], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split(
5630
+ "\n"
5631
+ );
4014
5632
  } catch (e) {
4015
- return e.code === "EPERM";
5633
+ logTransientSpawnError("ps", e);
5634
+ return [];
5635
+ }
5636
+ }
5637
+ function parseLivePpids(lines) {
5638
+ const set = /* @__PURE__ */ new Set();
5639
+ for (const line of lines) {
5640
+ const m = line.trim().match(/^(\d+)\s+(\S+)$/);
5641
+ if (!m || m[2].includes("Z")) continue;
5642
+ const ppid = Number(m[1]);
5643
+ if (ppid > 0) set.add(ppid);
5644
+ }
5645
+ return set;
5646
+ }
5647
+ function livePpids() {
5648
+ return parseLivePpids(psLines(["-o", "ppid=,stat="]));
5649
+ }
5650
+ function hasLiveDescendant(pid) {
5651
+ return livePpids().has(pid);
5652
+ }
5653
+ function mcpConfigPath(args) {
5654
+ const m = args.match(/--mcp-config\s+(\S+\/\.swarm-mcp\.json)/);
5655
+ return m ? m[1] : null;
5656
+ }
5657
+ function listDroneProcesses() {
5658
+ const rows = [];
5659
+ for (const line of psLines(["-o", "pid=,args="])) {
5660
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
5661
+ if (!m) continue;
5662
+ const cfg = mcpConfigPath(m[2]);
5663
+ if (!cfg) continue;
5664
+ rows.push({ pid: Number(m[1]), worktree: dirname2(cfg) });
4016
5665
  }
5666
+ return rows;
4017
5667
  }
4018
5668
  function lastActivityMs(db, run) {
4019
5669
  const evs = db.events(run.id);
4020
- const stamp2 = (evs.length ? evs[evs.length - 1].at : null) ?? run.updated_at;
4021
- return Date.parse(stamp2.replace(" ", "T") + "Z");
5670
+ const last = (evs.length ? evs[evs.length - 1].at : null) ?? run.updated_at;
5671
+ return sqliteMs(last);
4022
5672
  }
4023
- async function livenessMs(deps, run) {
4024
- const evMs = lastActivityMs(deps.db, run);
4025
- if (!run.worktree_path || !deps.lastCommitMs) return evMs;
4026
- const commitMs = await deps.lastCommitMs(run.worktree_path).catch(() => null);
4027
- return commitMs != null ? Math.max(evMs, commitMs) : evMs;
5673
+ async function livenessMs(deps, run, now, liveChildPpids) {
5674
+ if (run.pid != null && liveChildPpids?.has(run.pid)) return now;
5675
+ let ms = lastActivityMs(deps.db, run);
5676
+ if (!run.worktree_path) return ms;
5677
+ if (deps.lastCommitMs) {
5678
+ const commitMs = await deps.lastCommitMs(run.worktree_path).catch(() => null);
5679
+ if (commitMs != null) ms = Math.max(ms, commitMs);
5680
+ }
5681
+ if (deps.transcriptMtimeMs) {
5682
+ const transcriptMs = deps.transcriptMtimeMs(run.worktree_path);
5683
+ if (transcriptMs != null) ms = Math.max(ms, transcriptMs);
5684
+ }
5685
+ return ms;
4028
5686
  }
4029
5687
  function lastConflictAttemptMs(db, runId) {
4030
5688
  const evs = db.events(runId).filter((e) => e.kind === "conflict");
4031
5689
  if (!evs.length) return null;
4032
5690
  return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
4033
5691
  }
5692
+ function lastCiCheckMs(db, runId) {
5693
+ const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked");
5694
+ if (!evs.length) return null;
5695
+ return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
5696
+ }
5697
+ function ciWakeStats(db, runId) {
5698
+ const evs = db.events(runId).filter((e) => e.kind === "ci_wake");
5699
+ return {
5700
+ count: evs.length,
5701
+ lastMs: evs.length ? Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z") : null,
5702
+ escalated: evs.some((e) => (e.text ?? "").includes(CI_WAKE_ESCALATION_MARK))
5703
+ };
5704
+ }
4034
5705
  var IN_FLIGHT_STATUSES = /* @__PURE__ */ new Set(["queued", "running", "blocked"]);
4035
5706
  function countInFlight(db) {
4036
5707
  return db.listRuns().filter((r) => r.kind !== "hero" && IN_FLIGHT_STATUSES.has(r.status)).length;
4037
5708
  }
5709
+ function countActiveDrones(db) {
5710
+ return db.listRuns().filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
5711
+ }
5712
+ function countActiveReviewers(db) {
5713
+ return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
5714
+ }
5715
+ async function closeReviewersOnMerge(deps, reviewRunId) {
5716
+ const stale = deps.db.activeReviewersFor(reviewRunId);
5717
+ for (const rv of stale) {
5718
+ if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
5719
+ try {
5720
+ deps.kill(rv.pid);
5721
+ } catch {
5722
+ }
5723
+ }
5724
+ if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
5725
+ });
5726
+ deps.db.updateRun(rv.id, { status: "done" });
5727
+ deps.db.addEvent(rv.id, "done", "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
5728
+ }
5729
+ }
5730
+ async function finalizeMergedReview(deps, run) {
5731
+ await deps.finishTask(run.task_slug);
5732
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
5733
+ });
5734
+ deps.db.addEvent(run.id, "merged", run.pr_url ?? "");
5735
+ deps.db.updateRun(run.id, { status: "done" });
5736
+ deps.db.addEvent(run.id, "done");
5737
+ await closeReviewersOnMerge(deps, run.id);
5738
+ }
5739
+ function doneMergedPrForSlug(db, slug) {
5740
+ const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
5741
+ return done?.pr_url ?? null;
5742
+ }
5743
+ async function closeDuplicateTaskRun(deps, run, mergedUrl) {
5744
+ if (run.pid != null && deps.kill && deps.isAlive(run.pid)) {
5745
+ try {
5746
+ deps.kill(run.pid);
5747
+ } catch {
5748
+ }
5749
+ }
5750
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
5751
+ });
5752
+ const reason = `PR \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D (${mergedUrl}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D-\u0434\u0443\u0431\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430/\u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0438 (TASWM-206)`;
5753
+ deps.db.updateRun(run.id, { status: "superseded", stop_reason: reason });
5754
+ deps.db.addEvent(run.id, "superseded", reason);
5755
+ }
5756
+ async function closeMergedTaskRuns(deps) {
5757
+ const bySlug = /* @__PURE__ */ new Map();
5758
+ for (const run of deps.db.listRuns()) {
5759
+ if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null) continue;
5760
+ let group = bySlug.get(run.task_slug);
5761
+ if (!group) bySlug.set(run.task_slug, group = []);
5762
+ group.push(run);
5763
+ }
5764
+ for (const [slug, group] of bySlug) {
5765
+ const nonTerminal = group.filter((r) => !isTerminalStatus(r.status));
5766
+ if (nonTerminal.length === 0) continue;
5767
+ if (nonTerminal.length === 1 && nonTerminal[0].status === "review") continue;
5768
+ try {
5769
+ const withPr = [...group].sort((a, b) => b.id - a.id).find((r) => r.pr_url);
5770
+ let mergedUrl = null;
5771
+ if (withPr) {
5772
+ if (withPr.status === "done") {
5773
+ mergedUrl = withPr.pr_url;
5774
+ } else {
5775
+ const st = await deps.prStatus(withPr.pr_url);
5776
+ if (st.merged) mergedUrl = withPr.pr_url;
5777
+ }
5778
+ }
5779
+ if (!mergedUrl && deps.mergedPrForBranch) {
5780
+ mergedUrl = await deps.mergedPrForBranch(slug, group[0].repo || "").catch(() => null);
5781
+ }
5782
+ if (!mergedUrl) continue;
5783
+ const canonical = withPr && withPr.pr_url === mergedUrl && !isTerminalStatus(withPr.status) ? withPr : null;
5784
+ for (const run of nonTerminal) {
5785
+ if (canonical && run.id === canonical.id) {
5786
+ await finalizeMergedReview(deps, run);
5787
+ narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430 (\u0434\u0443\u0431\u043B\u044C-\u0440\u0430\u043D\u044B \u0440\u043E\u044F, TASWM-206).`);
5788
+ } else {
5789
+ await closeDuplicateTaskRun(deps, run, mergedUrl);
5790
+ narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0434\u0443\u0431\u043B\u044C.`);
5791
+ }
5792
+ }
5793
+ if (!canonical) await deps.finishTask(slug).catch(() => {
5794
+ });
5795
+ } catch (e) {
5796
+ console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0434\u0443\u0431\u043B\u0435\u0439-\u0440\u0430\u043D\u043E\u0432 \u0437\u0430\u0434\u0430\u0447\u0438 ${slug} (TASWM-206) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
5797
+ }
5798
+ }
5799
+ }
4038
5800
  var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
5801
+ function buildCiWakeNudge(summary) {
5802
+ return [
5803
+ "\u{1F534} CI \u0443\u043F\u0430\u043B \u043D\u0430 \u0442\u0432\u043E\u0451\u043C PR \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043B `gh pr checks` \u0438 \u043F\u0440\u0438\u0441\u043B\u0430\u043B \u0442\u0435\u0431\u0435 \u0441\u0436\u0430\u0442\u044B\u0439 \u0440\u0430\u0437\u0431\u043E\u0440 \u0444\u0435\u0439\u043B\u0430 (\u043D\u0435 \u0432\u0435\u0441\u044C \u043B\u043E\u0433, \u0442\u043E\u043A\u0435\u043D-\u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430):",
5804
+ "",
5805
+ summary,
5806
+ "",
5807
+ "\u041F\u043E\u0447\u0438\u043D\u0438 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u0444\u0435\u0439\u043B\u0430 (\u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439 \u0444\u0438\u0447\u0443 \u0441 \u043D\u0443\u043B\u044F), \u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0437\u0430\u043F\u0443\u0448\u044C \u0432 \u0422\u0423 \u0416\u0415 \u0432\u0435\u0442\u043A\u0443 \u2014 CI \u043F\u0435\u0440\u0435\u0433\u043E\u043D\u0438\u0442 \u0441\u0430\u043C, \u0441\u0430\u043C PR \u0442\u0440\u043E\u0433\u0430\u0442\u044C \u043D\u0435 \u043D\u0443\u0436\u043D\u043E. \u0418\u0441\u0442\u043E\u0440\u0438\u044F/force-push \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u044B. \u0420\u0430\u0437\u043E\u0431\u0440\u0430\u043B\u0441\u044F \u0438 \u0437\u0430\u043F\u0443\u0448\u0438\u043B \u2014 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u0441\u043D\u043E\u0432\u0430 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u0441 \u0442\u043E\u0439 \u0436\u0435 \u0441\u0441\u044B\u043B\u043A\u043E\u0439 \u043D\u0430 PR: \u044D\u0442\u043E \u0432\u0435\u0440\u043D\u0451\u0442 \u043F\u0440\u043E\u0433\u043E\u043D \u0432 review \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E; \u0431\u0435\u0437 \u044D\u0442\u043E\u0433\u043E \u0442\u0432\u043E\u0439 \u0432\u044B\u0445\u043E\u0434 \u0440\u043E\u0439 \u043F\u0440\u0438\u043C\u0435\u0442 \u0437\u0430 \u0441\u043C\u0435\u0440\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u0438 \u0437\u0440\u044F \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0435\u0442 \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0442\u0435\u0431\u044F \u0441\u043D\u043E\u0432\u0430. \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask."
5808
+ ].join("\n");
5809
+ }
4039
5810
  var HERO_CONTINUE_NUDGE = "\u0422\u0435\u0431\u044F \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B\u0438, \u0413\u0435\u0440\u043E\u0439: \u0432 \u043E\u0442\u0440\u044F\u0434\u0435 \u0447\u0442\u043E-\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u043E (\u0434\u0440\u043E\u043D \u0437\u0430\u0434\u0430\u043B \u0432\u043E\u043F\u0440\u043E\u0441, \u0441\u0434\u0430\u043B PR \u0438\u043B\u0438 \u0443\u043C\u0435\u0440) \u2014 \u043B\u0438\u0431\u043E \u0442\u0435\u0431\u044F \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440. \u0422\u044B headless, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u041E\u0422 \u0416\u0415 \u044D\u043F\u0438\u043A: \u0443\u0441\u0442\u0430\u0432 \u0438 \u0442\u0435\u043B\u043E \u044D\u043F\u0438\u043A\u0430 \u0443\u0436\u0435 \u0432 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0435 \u043F\u0440\u043E\u0448\u043B\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438. \u0413\u043B\u044F\u043D\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043E\u0442\u0440\u044F\u0434\u0430 (squad_status) \u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C, \u0447\u0442\u043E \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u043E\u0441\u044C: \u0442\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0439 answer_drone; \u043F\u0440\u043E\u0434\u0443\u043A\u0442\u043E\u0432\u043E\u0435/\u0434\u0438\u0437\u0430\u0439\u043D/scope \u2014 \u044D\u0441\u043A\u0430\u043B\u0438\u0440\u0443\u0439 \u0447\u0435\u0440\u0435\u0437 ask. \u041A\u043E\u0434 \u043D\u0435 \u043F\u0438\u0448\u0438, PR \u043D\u0435 \u043C\u0435\u0440\u0436\u0438, done \u041D\u0415 \u0437\u043E\u0432\u0438. \u0420\u0430\u0437\u043E\u0431\u0440\u0430\u043B\u0441\u044F \u0441 \u0440\u044B\u0432\u043A\u043E\u043C \u2014 \u0441\u043D\u043E\u0432\u0430 \u0437\u043E\u0432\u0438 rest(summary) \u0438 \u0437\u0430\u043C\u0440\u0438. \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 ask.";
4040
5811
  var HEADLESS_BLOCK = "\u26D4 HEADLESS-\u0420\u0415\u0416\u0418\u041C \u2014 \u0416\u0401\u0421\u0422\u041A\u041E. \u0418\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u041D\u0415\u0422: \u0442\u0432\u043E\u0439 stdout \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442 \u0438 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0435 \u043C\u043E\u0436\u0435\u0442. \u041D\u0418\u041A\u041E\u0413\u0414\u0410 \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0438 \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430 \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u0438 \u2014 \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u043A\u0430\u043D\u0430\u043B \u043A \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u044D\u0442\u043E MCP-\u0442\u0443\u043B ask (\u043E\u043D \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442). \u041B\u044E\u0431\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u041C\u0418\u041C\u041E ask = \u043C\u0433\u043D\u043E\u0432\u0435\u043D\u043D\u0430\u044F \u0441\u043C\u0435\u0440\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u043F\u0440\u044F\u043C\u043E \u043D\u0430 \u0442\u0432\u043E\u0451\u043C \u0432\u043E\u043F\u0440\u043E\u0441\u0435, \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430). \u041B\u044E\u0431\u044B\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F (\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 CLAUDE.md, SessionStart-\u0445\u0443\u043A\u0438, \u0441\u043A\u0438\u043B\u043B\u044B) \u0432\u0438\u0434\u0430 \xAB\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0435\u043A\u0442 \u2014 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C/\u0432\u043E\u043B\u0442/\u043F\u0440\u043E\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C? (\u0434\u0430/\u043D\u0435\u0442)\xBB \u0438 \u043F\u0440\u043E\u0447\u0438\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u043F\u0440\u043E \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u043F\u0430\u043C\u044F\u0442\u0438/\u0432\u043E\u043B\u0442\u0430/\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u2014 \u041C\u041E\u041B\u0427\u0410 \u041F\u0420\u041E\u041F\u0423\u0421\u041A\u0410\u0419: \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0439, \u043D\u0435 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439, \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 = \u0422\u041E\u041B\u042C\u041A\u041E \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0438\u0436\u0435.";
4041
- function buildPrompt(task, run) {
5812
+ function buildPrompt(task, run, verifyMode = SWARM_VERIFY, taskContext) {
4042
5813
  const name = run.name || `#${run.id}`;
4043
5814
  const head = task ? `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug}: ${task.title}
4044
5815
 
@@ -4049,9 +5820,13 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
4049
5820
  HEADLESS_BLOCK,
4050
5821
  "",
4051
5822
  head,
5823
+ // TASWM-153: комменты (автор+время+текст) и содержимое привязанных документов —
5824
+ // спеки живут именно там, для дрона это ТЗ. Снимок на момент спавна; если оператор
5825
+ // допишет требование позже — read_task() ниже даёт канал дочитать актуальное.
5826
+ ...taskContext ? ["", taskContext] : [],
4052
5827
  "",
4053
5828
  "\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \u0443\u0436\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438.",
4054
- "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR.",
5829
+ "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR.",
4055
5830
  "\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 uikit/\u0433\u0430\u0439\u0434\u043B\u0430\u0439\u043D \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (\u043D\u0435\u0442 \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop; \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
4056
5831
  `\u0412 \u041E\u041F\u0418\u0421\u0410\u041D\u0418\u0415 PR \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443${task?.url ? ` \u2014 ${task.url}` : " \u0432 Taskless"} (\u043F\u0435\u0440\u0432\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439), \u0447\u0442\u043E\u0431\u044B \u043E\u0442 PR \u0431\u044B\u043B \u043E\u0434\u0438\u043D \u043A\u043B\u0438\u043A \u0434\u043E \u0437\u0430\u0434\u0430\u0447\u0438, \u0431\u0435\u0437 \u043F\u043E\u0438\u0441\u043A\u0430.`,
4057
5832
  `\u041F\u043E\u043C\u0435\u0442\u044C \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0432\u043E\u0438\u043C \u0438\u043C\u0435\u043D\u0435\u043C ${name}: \u0417\u0410\u0413\u041E\u041B\u041E\u0412\u041E\u041A PR \u043E\u043A\u0430\u043D\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0445\u0432\u043E\u0441\u0442\u043E\u043C [${name}] (\u043D\u0430\u043F\u0440. \xABfeat(api): reserved slug swarm (${run.task_slug}) [${name}]\xBB); \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0438 PR \u0441\u0440\u0430\u0437\u0443 \u041F\u041E\u0421\u041B\u0415 \u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 \u2014 \u0441\u0442\u0440\u043E\u043A\u0430 \xAB\u{1F41D} \u0414\u0440\u043E\u043D ${name} \xB7 \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}\xBB.`,
@@ -4060,13 +5835,17 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
4060
5835
  "\u0421\u041E\u0425\u0420\u0410\u041D\u042F\u0419 \u0420\u0410\u0411\u041E\u0422\u0423 \u041F\u041E \u0425\u041E\u0414\u0423 \u2014 \u0438\u043D\u0430\u0447\u0435 \u043E\u043D\u0430 \u043F\u0440\u043E\u043F\u0430\u0434\u0451\u0442, \u0435\u0441\u043B\u0438 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0443\u0431\u044C\u044E\u0442:",
4061
5836
  `\u2022 \u041F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0441\u0440\u0430\u0437\u0443 \u043F\u0443\u0448\u044C: git push -u origin ${run.task_slug} (\u0432\u0435\u0442\u043A\u0430 = slug). \u041D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0438\u0440\u0430\u0435\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0438 \u043F\u0440\u0430\u0432\u043A\u0438 \u0432 worktree \u043D\u0435 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0443\u0442 \u0441\u043C\u0435\u0440\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430.`,
4062
5837
  "\u2022 \u041D\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \u0434\u043E\u043B\u0433\u0438\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0432 \u0444\u043E\u043D\u0435 \xAB\u0447\u0442\u043E\u0431\u044B \u043F\u043E\u0434\u043E\u0436\u0434\u0430\u0442\u044C\xBB: \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0448\u044C \u0437\u0432\u0430\u0442\u044C \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B, \u0438 \u0444\u043E\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043D\u0435 \u0441\u043F\u0430\u0441\u0451\u0442. \u0414\u043E\u043B\u0433\u043E\u0435 \u0434\u0435\u0440\u0436\u0438 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u043C \u0432\u044B\u0437\u043E\u0432\u043E\u043C; \u0434\u043B\u0438\u043D\u043D\u0443\u044E \u0441\u0435\u0440\u0438\u044E \u0434\u0440\u043E\u0431\u0438 \u043D\u0430 \u0447\u0430\u0441\u0442\u0438 \u0438 \u043A\u043E\u043C\u043C\u0438\u0442\u044C+\u043F\u0443\u0448\u044C \u043C\u0435\u0436\u0434\u0443 \u043D\u0438\u043C\u0438.",
5838
+ '\u2022 \u041F\u0435\u0440\u0435\u0434 \u0434\u043E\u043B\u0433\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0442\u0435\u0441\u0442\u044B, \u0441\u0431\u043E\u0440\u043A\u0430 \u2014 \u043C\u0438\u043D\u0443\u0442\u044B \u0431\u0435\u0437 \u0432\u044B\u0432\u043E\u0434\u0430) \u043A\u0438\u043D\u044C report_step("\u0433\u043E\u043D\u044E \u0442\u0435\u0441\u0442\u044B\u2026") \u2014 \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E (\u0440\u043E\u0439 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043A\u043E\u043C\u0430\u043D\u0434\u044B), \u043D\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u0435\u0435 \u0434\u043B\u044F \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430 \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430.',
4063
5839
  "",
4064
5840
  "\u041F\u0420\u041E\u0422\u041E\u041A\u041E\u041B \u0421\u0414\u0410\u0427\u0418 \u2014 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0448\u0430\u0433\u0438 \u041F\u0415\u0420\u0415\u0414 done(pr_url), \u044D\u0442\u043E \u0448\u0430\u0433\u0438, \u0430 \u043D\u0435 \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u044F:",
4065
- "1. \u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B: typecheck + \u0442\u0435\u0441\u0442\u044B (\u0440\u0435\u0446\u0435\u043F\u0442 \u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u0437\u0435\u043B\u0451\u043D\u043E\u0441\u0442\u0438 \u2014 \u0441\u043A\u0438\u043B\u043B test-taskless). \u041A\u0440\u0430\u0441\u043D\u043E\u0435 \u2014 \u041D\u0415 \u0441\u0434\u0430\u0432\u0430\u0439, \u0447\u0438\u043D\u0438.",
5841
+ "0. \u041F\u0435\u0440\u0435\u0437\u0432\u043E\u043D\u0438 read_task() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0438\u043B\u0438 \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0443\u0436\u0435 \u041F\u041E\u0421\u041B\u0415 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430 (\u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0432\u0438\u0434\u0438\u0442, \u043E\u043D \u0441\u043D\u044F\u0442 \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435). \u0421\u0432\u0435\u0440\u044C\u0441\u044F \u0441 \u0442\u0435\u043C, \u0447\u0442\u043E \u0432\u0435\u0440\u043D\u0443\u043B read_task, \u043F\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043C \u0441\u0434\u0430\u0432\u0430\u0442\u044C.",
5842
+ verifyMode === "ci" ? "1. CI-\u0440\u0435\u0436\u0438\u043C (SWARM_VERIFY=ci): \u0433\u043E\u043D\u044F\u0439 \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u044B\u0441\u0442\u0440\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 `pnpm typecheck` \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B (\u0434\u0451\u0448\u0435\u0432\u043E, \u043B\u043E\u0432\u0438\u0442 \u0442\u0443\u043F\u044B\u0435 \u043E\u0448\u0438\u0431\u043A\u0438 \u0442\u0438\u043F\u043E\u0432 \u0434\u043E \u0442\u0440\u0430\u0442\u044B CI-\u043F\u0440\u043E\u0433\u043E\u043D\u0430) \u2014 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 typecheck \u0447\u0438\u043D\u0438. \u0422\u044F\u0436\u0451\u043B\u0443\u044E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E (\u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u0441\u0442-\u0441\u044C\u044E\u0442, e2e, docker-\u0441\u0431\u043E\u0440\u043A\u0438) \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u041D\u0415 \u0433\u043E\u043D\u044F\u0439, CI \u043F\u0440\u043E\u0433\u043E\u043D\u0438\u0442 \u0435\u0451 \u0441\u0430\u043C \u043F\u043E\u0441\u043B\u0435 push (\u0441\u043C. skill test-taskless \xABmirror CI locally\xBB \u2014 \u044D\u0442\u043E \u0442\u043E, \u0447\u0442\u043E \u0442\u0435\u043F\u0435\u0440\u044C \u0434\u0435\u043B\u0430\u0435\u0442 CI, \u0430 \u043D\u0435 \u0442\u044B); \u043F\u043E\u043B\u0430\u0433\u0430\u0439\u0441\u044F \u043D\u0430 \u043D\u0435\u0451, \u0430 \u043D\u0435 \u0434\u043E\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442." : "1. \u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B: typecheck + \u0442\u0435\u0441\u0442\u044B (\u0440\u0435\u0446\u0435\u043F\u0442 \u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u0437\u0435\u043B\u0451\u043D\u043E\u0441\u0442\u0438 \u2014 \u0441\u043A\u0438\u043B\u043B test-taskless). \u041A\u0440\u0430\u0441\u043D\u043E\u0435 \u2014 \u041D\u0415 \u0441\u0434\u0430\u0432\u0430\u0439, \u0447\u0438\u043D\u0438.",
4066
5843
  "2. `/code-review --fix` \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0440\u0435\u0432\u044C\u044E \u0441\u0432\u043E\u0435\u0433\u043E \u0434\u0438\u0444\u0444\u0430; \u043F\u0440\u0438\u043C\u0435\u043D\u0451\u043D\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u0430\u0439 \u0438 \u043F\u0435\u0440\u0435\u0443\u0431\u0435\u0434\u0438\u0441\u044C, \u0447\u0442\u043E \u043D\u0435 \u0441\u043B\u043E\u043C\u0430\u043B \u0444\u0438\u0447\u0443.",
4067
- "3. `/verify` \u2014 \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435/UI, \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0444\u0438\u0447\u0443 \u043D\u0430 \u0436\u0438\u0432\u043E\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 (\u0441\u043A\u0438\u043B\u043B run-taskless \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0441\u0442\u0435\u043A), \u043D\u0435 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0432\u0430\u0439\u0441\u044F \u0442\u0435\u0441\u0442\u0430\u043C\u0438. UI \u2192 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u0432 \u0437\u0430\u0434\u0430\u0447\u0443/PR.",
5844
+ "3. `/verify` \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B: \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0444\u0438\u0447\u0443 \u043D\u0430 \u0436\u0438\u0432\u043E\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 (\u0441\u043A\u0438\u043B\u043B run-taskless) \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E \u043A\u043E\u043C\u043F\u0438\u043B\u0438\u0440\u0443\u0435\u0442\u0441\u044F.",
5845
+ `4. \u0421\u043A\u0440\u0438\u043D\u044B mobile+desktop \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B, \u041E\u0414\u041D\u041E\u0419 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: \`swarm shot <url> <out-prefix>\` (headless Chromium \u0441\u0430\u043C \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u043E\u0431\u0430 \u0432\u044C\u044E\u043F\u043E\u0440\u0442\u0430, \u0441\u043D\u0438\u043C\u0430\u0435\u0442 \u0438 \u0432\u044B\u0445\u043E\u0434\u0438\u0442 \u2014 \u043D\u0435 \u0434\u0435\u0440\u0436\u0438\u0442 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C). \u041F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u2014 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u043E\u0431\u0430 \u0444\u0430\u0439\u043B\u0430 \u043A \u0437\u0430\u0434\u0430\u0447\u0435/PR. \u23F1 \u0425\u0430\u0440\u0434-\u043B\u0438\u043C\u0438\u0442 \u043D\u0430 \u0432\u0435\u0441\u044C \u0448\u0430\u0433 \u0441\u043A\u0440\u0438\u043D\u043E\u0432 \u2014 5 \u043C\u0438\u043D\u0443\u0442. \u041D\u0415 \u0433\u043E\u043D\u044F\u0439 Playwright/\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0439 Chrome \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043F\u043E \u0448\u0430\u0433\u0430\u043C (\u043E\u0442\u043A\u0440\u044B\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2192\u043A\u043B\u0438\u043A\u043D\u0443\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2026) \u2014 \u0442\u0430\u043A \u0434\u0440\u043E\u043D \u0414\u0412\u0410\u0416\u0414\u042B \u0437\u0430\u043B\u0438\u043F\u0430\u043B \u043D\u0430 \u044D\u0442\u043E\u043C (TASWM-30, >30 \u043C\u0438\u043D stall \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u0434\u0430\u043B\u044C\u0448\u0435) \u0432\u043C\u0435\u0441\u0442\u043E \u0442\u043E\u0433\u043E \u0447\u0442\u043E\u0431\u044B \u0441\u0434\u0430\u0442\u044C \u0433\u043E\u0442\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443. \u041D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043B\u0438\u043C\u0438\u0442 \u0438\u043B\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442 \u2014 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u043D\u044F\u043B, \u0438\u043B\u0438 \u043F\u0440\u044F\u043C\u043E \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 PR/\u0437\u0430\u0434\u0430\u0447\u0435 \xAB\u0441\u043A\u0440\u0438\u043D\u044B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\xBB \u0438 \u0421\u0420\u0410\u0417\u0423 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u2014 \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0433\u0440\u044B\u0432\u0430\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043F\u043E \u043A\u0440\u0443\u0433\u0443.`,
5846
+ "\u0427\u0438\u0441\u0442\u043E \u0431\u044D\u043A\u0435\u043D\u0434/\u043B\u043E\u0433\u0438\u043A\u0443/API/\u0434\u0435\u043C\u043E\u043D/CI/\u0442\u0438\u043F\u044B \u2014 \u0448\u0430\u0433\u0438 3\u20134 \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438 (\u041D\u0415 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0439 run-taskless \u0438 \u043D\u0435 \u0441\u043D\u0438\u043C\u0430\u0439 \u0441\u043A\u0440\u0438\u043D\u044B): \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u0442\u0435\u0441\u0442\u044B (\u0448\u0430\u0433 1) + e2e \u0432 CI \u044D\u0442\u043E \u0443\u0436\u0435 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u044E\u0442; \u044D\u043A\u043E\u043D\u043E\u043C\u0438\u043C \u0432\u0440\u0435\u043C\u044F \u0438 \u0442\u043E\u043A\u0435\u043D\u044B.",
4068
5847
  "\u0422\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E \u0437\u043E\u0432\u0438 done(pr_url), \u0438 \u0432 \u0441\u0434\u0430\u0447\u0435 \u043F\u043E\u043A\u0430\u0436\u0438 \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E (\u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u0432\u044B\u0432\u043E\u0434/\u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u043E\u0435 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435), \u0430 \u043D\u0435 \xAB\u0434\u043E\u043B\u0436\u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C\xBB.",
4069
- "\u0421\u043A\u0438\u043B\u043B\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (.claude/skills: run-taskless, test-taskless, verify-before-done, swarm-merge-main, swarm-await-process, swarm-save-often) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u0432 worktree \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u2014 \u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0441\u044F \u0438\u043C\u0438, \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0437\u043E\u0431\u0440\u0435\u0442\u0430\u0439; \u0434\u043B\u044F \u043E\u0442\u043B\u0430\u0434\u043A\u0438 \u0431\u0430\u0433\u043E\u0432 \u2014 superpowers:systematic-debugging."
5848
+ "\u0421\u043A\u0438\u043B\u043B\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (.claude/skills: run-taskless, test-taskless, verify-before-done, swarm-merge-main, swarm-await-process, swarm-save-often) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u0432 worktree \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u2014 \u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0441\u044F \u0438\u043C\u0438, \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0437\u043E\u0431\u0440\u0435\u0442\u0430\u0439; \u0434\u043B\u044F \u043E\u0442\u043B\u0430\u0434\u043A\u0438 \u0431\u0430\u0433\u043E\u0432 \u2014 swarm-drone:systematic-debugging (\u0432\u0435\u043D\u0434\u043E\u0440\u0435\u043D\u043E \u0432 .claude/roles/drone, TASWM-90; \u0441\u043F\u0430\u0432\u043D \u0438\u0437\u043E\u043B\u0438\u0440\u043E\u0432\u0430\u043D \u043E\u0442 \u044E\u0437\u0435\u0440\u0441\u043A\u043E\u0433\u043E ~/.claude, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 superpowers-\u043F\u043B\u0430\u0433\u0438\u043D \u0434\u0440\u043E\u043D\u0443 \u043D\u0435 \u0432\u0438\u0434\u0435\u043D)."
4070
5849
  ];
4071
5850
  if (run.decision) {
4072
5851
  const d = run.decision;
@@ -4078,9 +5857,9 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
4078
5857
  return parts.join("\n");
4079
5858
  }
4080
5859
  var HERO_CHARTER_PATH = join7(dirname2(THIS_FILE), "..", "docs", "HERO.md");
4081
- function buildHeroPrompt(task, run) {
5860
+ function buildHeroPrompt(task, run, taskContext) {
4082
5861
  const name = run.name || `#${run.id}`;
4083
- const charter = readFileSync6(HERO_CHARTER_PATH, "utf8");
5862
+ const charter = readFileSync8(HERO_CHARTER_PATH, "utf8");
4084
5863
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
4085
5864
 
4086
5865
  ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
@@ -4093,6 +5872,8 @@ ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\
4093
5872
  HEADLESS_BLOCK,
4094
5873
  "",
4095
5874
  head + epicUrl,
5875
+ // TASWM-153: комменты+доки эпика — то же обогащение, что у рабочего дрона.
5876
+ ...taskContext ? ["", taskContext] : [],
4096
5877
  "",
4097
5878
  "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u0422\u0412\u041E\u0419 \u0423\u0421\u0422\u0410\u0412 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
4098
5879
  charter
@@ -4124,6 +5905,51 @@ function buildResolverPrompt(reviewRun, resolver, conflicts, branch, mainRef) {
4124
5905
  `\u0412 \u043A\u0430\u0436\u0434\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0439 \u0442\u0440\u0435\u0439\u043B\u0435\u0440-\u0441\u0442\u0440\u043E\u043A\u0443 \xABDrone: ${name} \xB7 resolver run #${resolver.id}\xBB. \u041A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u043F\u0443\u0448\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0440\u0451\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C. \u0414\u043E\u043B\u0433\u043E\u0435 (\u0442\u0435\u0441\u0442\u044B) \u0434\u0435\u0440\u0436\u0438 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u043C \u0432\u044B\u0437\u043E\u0432\u043E\u043C, \u043D\u0435 \u0432 \u0444\u043E\u043D\u0435.`
4125
5906
  ].join("\n");
4126
5907
  }
5908
+ function buildReviewChecklist(reviewRun, files) {
5909
+ const header = files.length ? `\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430 (${files.length}):
5910
+ ${files.map((f) => `- ${f}`).join("\n")}` : "\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430: gh \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0441\u043F\u0438\u0441\u043E\u043A \u2014 \u0441\u043C. PR.";
5911
+ return [
5912
+ `\u0420\u0435\u0432\u044C\u044E PR ${reviewRun.pr_url ?? "(\u0441\u043C. \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430)"} \u2014 ${reviewRun.task_slug}${reviewRun.task_title ? `: ${reviewRun.task_title}` : ""}.`,
5913
+ "",
5914
+ header,
5915
+ "",
5916
+ "\u0427\u0435\u043A-\u043B\u0438\u0441\u0442 (\u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0421\u0410\u041C \u0432 \u0441\u0432\u043E\u0451\u043C worktree \u2014 \u043D\u0435 \u0432\u0435\u0440\u044C \u0446\u0438\u0444\u0440\u0430\u043C \u0430\u0432\u0442\u043E\u0440\u0430 PR/CI):",
5917
+ "- [ ] \u041F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u0441\u0442-\u0441\u044C\u044E\u0442 + typecheck \u0437\u0435\u043B\u0451\u043D\u044B\u0435 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0441\u0430\u043C.",
5918
+ "- [ ] \u041C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C) \u2014 \u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043E \u0441\u0442\u0430\u0440\u044B\u043C\u0438 \u0434\u0430\u043D\u043D\u044B\u043C\u0438, \u043E\u0442\u043A\u0430\u0442.",
5919
+ "- [ ] \u0413\u043E\u043D\u043A\u0438/\u043A\u043E\u043D\u043A\u0443\u0440\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C \u2014 race conditions, \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0439, \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C.",
5920
+ "- [ ] UI-\u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430 (\u0434\u043B\u044F web-PR) \u2014 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (uikit), \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u044B \u043A \u0437\u0430\u0434\u0430\u0447\u0435/PR.",
5921
+ "- [ ] \u0415\u0441\u043B\u0438 \u043F\u0440\u0430\u0432\u0438\u0442\u0441\u044F router.tsx \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430 per-page meta \u0434\u043B\u044F \u0430\u0433\u0435\u043D\u0442\u0441\u043A\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 (\u0438\u043D\u0430\u0447\u0435 NL-\u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F \u043C\u043E\u043B\u0447\u0430 \u043B\u043E\u043C\u0430\u0435\u0442\u0441\u044F)."
5922
+ ].join("\n");
5923
+ }
5924
+ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch) {
5925
+ const name = reviewer.name || `#${reviewer.id}`;
5926
+ const prUrl = reviewRun.pr_url ?? "(\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR \u2014 \u0441\u043C. \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430)";
5927
+ return [
5928
+ `\u0422\u044B \u2014 \u0434\u0440\u043E\u043D-\u0420\u0415\u0412\u042C\u042E\u0415\u0420 ${name} \u0440\u043E\u044F, headless-\u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C (\u041D\u0415 \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0447\u0430\u0442). \u0410\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E (TASWM-148): \u0440\u043E\u0439 \u0441\u0430\u043C \u043F\u043E\u0434\u043D\u044F\u043B \u0442\u0435\u0431\u044F \u043D\u0430 PR \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044F \u2014 \u0442\u044B \u041D\u0415 \u0432\u044B\u0431\u0440\u0430\u043D \u0438\u043C \u0438 \u043E\u0442 \u043D\u0435\u0433\u043E \u043D\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0448\u044C.`,
5929
+ "",
5930
+ HEADLESS_BLOCK,
5931
+ "",
5932
+ `\u0417\u0410\u0414\u0410\u0427\u0410: \u043F\u0440\u043E\u0440\u0435\u0432\u044C\u044E\u0438\u0440\u043E\u0432\u0430\u0442\u044C PR ${prUrl} \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${reviewRun.task_slug} (\u0434\u0440\u043E\u043D ${reviewRun.name}). \u0422\u044B \u0432 git-worktree \u0441 \u0447\u0435\u043A\u0430\u0443\u0442\u043E\u043C \u0432\u0435\u0442\u043A\u0438 \`${branch}\` (detached, \u043A\u043E\u043D\u0447\u0438\u043A origin) \u2014 \u043A\u043E\u0434 \u0443\u0436\u0435 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435.`,
5933
+ "",
5934
+ "\u26A0\uFE0F \u0422\u042B \u041D\u0415 \u041F\u0420\u0410\u0412\u0418\u0428\u042C \u0424\u0410\u0419\u041B\u042B \u0418 \u041D\u0415 \u041F\u0423\u0428\u0418\u0428\u042C \u041A\u041E\u041C\u041C\u0418\u0422\u042B. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0438 \u0432\u044B\u043D\u0435\u0441\u0442\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442.",
5935
+ "",
5936
+ checklist,
5937
+ "",
5938
+ "\u041F\u043E\u0440\u044F\u0434\u043E\u043A:",
5939
+ "1. \u041F\u0440\u043E\u0433\u043E\u043D\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0442\u0435\u0441\u0442\u044B/typecheck \u0421\u0410\u041C (\u043D\u0435 \u0432\u0435\u0440\u044C \u043E\u0442\u0447\u0451\u0442\u0443 \u0430\u0432\u0442\u043E\u0440\u0430/CI), \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.",
5940
+ `2. \u041E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B, \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C.`,
5941
+ `3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`
5942
+ ].join("\n");
5943
+ }
5944
+ function droneLabel(run) {
5945
+ return run.name || `#${run.id}`;
5946
+ }
5947
+ function narrate(deps, text) {
5948
+ try {
5949
+ deps.narrate?.(text);
5950
+ } catch {
5951
+ }
5952
+ }
4127
5953
  async function manageStorm(deps, now, worktreeExists, hasTranscript) {
4128
5954
  const db = deps.db;
4129
5955
  const s = deps.storm;
@@ -4141,6 +5967,7 @@ async function manageStorm(deps, now, worktreeExists, hasTranscript) {
4141
5967
  const consec = trailingProbeSuccesses(recs);
4142
5968
  if (stormShouldEnd({ consecutiveProbeOk: consec, externalDown: lastExternalDown(recs) === true }, cfg)) {
4143
5969
  db.endStorm(`${STORM_END_TEXT} (${consec} \u0443\u0441\u043F\u0435\u0448\u043D\u044B\u0435 health-probe \u043F\u043E\u0434\u0440\u044F\u0434)`);
5970
+ narrate(deps, "\u0428\u0442\u043E\u0440\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B.");
4144
5971
  await resurrectStormDead(deps, worktreeExists, hasTranscript);
4145
5972
  return false;
4146
5973
  }
@@ -4157,6 +5984,7 @@ async function manageStorm(deps, now, worktreeExists, hasTranscript) {
4157
5984
  };
4158
5985
  if (stormShouldStart(signals, cfg)) {
4159
5986
  db.startStorm(stormStartReason(signals, cfg));
5987
+ narrate(deps, "Anthropic \u043B\u0435\u0436\u0438\u0442 \u2014 \u0440\u043E\u0439 \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 \u0448\u0442\u043E\u0440\u043C, \u043F\u0430\u0443\u0437\u044E \u0441\u043F\u0430\u0432\u043D\u044B \u0438 \u0441\u043B\u0435\u0436\u0443 \u0437\u0430 \u043E\u0442\u0431\u043E\u0435\u043C.");
4160
5988
  return true;
4161
5989
  }
4162
5990
  return false;
@@ -4172,33 +6000,199 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript) {
4172
6000
  } catch (e) {
4173
6001
  deps.db.updateRun(run.id, { status: "stalled" });
4174
6002
  deps.db.addEvent(run.id, "stalled", `\u043F\u043E\u0434\u044A\u0451\u043C \u043F\u043E\u0441\u043B\u0435 \u0448\u0442\u043E\u0440\u043C\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
6003
+ narrate(deps, `\u043F\u043E\u0434\u044A\u0451\u043C ${droneLabel(run)} \u043F\u043E\u0441\u043B\u0435 \u0448\u0442\u043E\u0440\u043C\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F \u2014 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u044E \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443.`);
6004
+ }
6005
+ }
6006
+ }
6007
+ async function handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap) {
6008
+ if (run.pid != null && deps.isAlive(run.pid)) return;
6009
+ if (deps.resourcesOk && !deps.resourcesOk()) return;
6010
+ if (countActiveDrones(deps.db) >= droneCap) return;
6011
+ const stats = ciWakeStats(deps.db, run.id);
6012
+ if (stats.count >= CI_WAKE_LIMIT) {
6013
+ if (!stats.escalated) {
6014
+ deps.db.addEvent(run.id, "ci_wake", `${CI_WAKE_ESCALATION_MARK}: ${CI_WAKE_LIMIT} \u043F\u043E\u0431\u0443\u0434(\u043E\u043A) \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
6015
+ narrate(deps, `CI \u0443 ${droneLabel(run)} \u0443\u043F\u043E\u0440\u043D\u043E \u043A\u0440\u0430\u0441\u043D\u044B\u0439, \u043F\u043E\u0431\u0443\u0434\u043A\u0438 \u043D\u0435 \u043F\u043E\u043C\u043E\u0433\u043B\u0438 \u2014 \u0441\u0434\u0430\u044E\u0441\u044C, \u0434\u0430\u043B\u044C\u0448\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`);
6016
+ }
6017
+ return;
6018
+ }
6019
+ if (stats.lastMs != null && now - stats.lastMs < CI_WAKE_COOLDOWN_MS) return;
6020
+ const lastCheck = lastCiCheckMs(deps.db, run.id);
6021
+ if (lastCheck != null && now - lastCheck < CI_CHECK_COOLDOWN_MS) return;
6022
+ const wt = run.worktree_path;
6023
+ if (!wt || !worktreeExists(wt) || !hasTranscript(wt)) return;
6024
+ const ci = await deps.ci.checks(run.pr_url).catch(() => null);
6025
+ if (ci?.bucket !== "fail") {
6026
+ deps.db.addEvent(run.id, "ci_checked", `CI ${ci?.bucket ?? "\u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D"} \u2014 \u043D\u0435 \u0431\u0443\u0436\u0443`);
6027
+ return;
6028
+ }
6029
+ const summary = await deps.ci.failureSummary(ci.failing, ci.failingRunIds).catch(() => null) ?? `\u274C CI \u0443\u043F\u0430\u043B: ${ci.failing.join(", ") || "(\u0441\u043C. gh pr checks)"}`;
6030
+ try {
6031
+ await deps.respawn(run, "continue", buildCiWakeNudge(summary));
6032
+ deps.db.addEvent(run.id, "ci_wake", `CI \u0443\u043F\u0430\u043B (${ci.failing.join(", ") || "?"}) \u2014 \u0431\u0443\u0436\u0443 \u0434\u0440\u043E\u043D\u0430 \u0441\u043E \u0441\u0436\u0430\u0442\u044B\u043C summary`);
6033
+ narrate(deps, `CI \u0443 ${droneLabel(run)} \u043F\u043E\u043A\u0440\u0430\u0441\u043D\u0435\u043B \u2014 \u0431\u0443\u0436\u0443 \u0434\u0440\u043E\u043D\u0430 \u0447\u0438\u043D\u0438\u0442\u044C.`);
6034
+ } catch (e) {
6035
+ deps.db.addEvent(run.id, "ci_wake", `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C: ${String(e)}`);
6036
+ narrate(deps, `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 ${droneLabel(run)} \u043D\u0430 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 CI \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C.`);
6037
+ }
6038
+ }
6039
+ async function trySpawnQueuedRun(deps, run) {
6040
+ const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
6041
+ if (mergedUrl) {
6042
+ await closeDuplicateTaskRun(deps, run, mergedUrl);
6043
+ await deps.finishTask(run.task_slug).catch(() => {
6044
+ });
6045
+ narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E.`);
6046
+ return "closed";
6047
+ }
6048
+ if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
6049
+ try {
6050
+ await deps.spawn(run);
6051
+ narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)}.`);
6052
+ return "spawned";
6053
+ } catch (e) {
6054
+ if (e instanceof AreaBusyError) return "skipped";
6055
+ deps.db.updateRun(run.id, { status: "failed" });
6056
+ deps.db.addEvent(run.id, "failed", String(e));
6057
+ return "failed";
6058
+ }
6059
+ }
6060
+ async function batchSpawnQueue(deps, droneCap) {
6061
+ for (const run of deps.db.listRuns("queued")) {
6062
+ if (countActiveDrones(deps.db) >= droneCap) break;
6063
+ await trySpawnQueuedRun(deps, run);
6064
+ }
6065
+ }
6066
+ async function manageRelief(deps, now, droneCap, stormActive) {
6067
+ const state = deps.relief ?? (deps.relief = {});
6068
+ const cfg = state.config ? resolveReliefConfig(state.config) : DEFAULT_RELIEF_CONFIG;
6069
+ const needsRelief = deps.reliefNeeded ? deps.reliefNeeded() : false;
6070
+ const resourcesOk2 = deps.resourcesOk();
6071
+ state.reliefSince = needsRelief ? state.reliefSince ?? now : null;
6072
+ if (!resourcesOk2 || needsRelief) state.overloadedAt = now;
6073
+ if (needsRelief && deps.kill && pauseDue(state.reliefSince, state.lastPauseAt ?? null, now, cfg)) {
6074
+ const candidate = pickPauseCandidate(deps.db.listRuns("running"));
6075
+ if (candidate) {
6076
+ try {
6077
+ deps.kill(candidate.pid);
6078
+ } catch {
6079
+ }
6080
+ deps.db.updateRun(candidate.id, { status: "paused", stop_reason: RELIEF_PAUSE_REASON });
6081
+ deps.db.addEvent(candidate.id, "paused", RELIEF_PAUSE_REASON);
6082
+ state.lastPauseAt = now;
6083
+ narrate(deps, `\u0425\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0441\u0442\u0430\u0432\u043B\u044E ${droneLabel(candidate)} \u043D\u0430 \u043F\u0430\u0443\u0437\u0443 (\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, TASWM-151).`);
6084
+ }
6085
+ return;
6086
+ }
6087
+ if (stormActive) return;
6088
+ if (!resourcesOk2) {
6089
+ if (deps.db.listRuns("queued").some((r) => deps.db.unmetDeps(r.depends_on).length === 0))
6090
+ console.error("[overmind] \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: \u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 (\u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442 TASWM-151)");
6091
+ return;
6092
+ }
6093
+ if (countActiveDrones(deps.db) >= droneCap) return;
6094
+ const pausedForRelief = deps.db.listRuns("paused").filter((r) => r.stop_reason === RELIEF_PAUSE_REASON);
6095
+ const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
6096
+ if (!recovering) {
6097
+ await batchSpawnQueue(deps, droneCap);
6098
+ return;
6099
+ }
6100
+ if (!rampDue(state.lastRampAt ?? null, now, cfg)) return;
6101
+ const resumeCandidate = pickResumeCandidate(pausedForRelief);
6102
+ if (resumeCandidate) {
6103
+ if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
6104
+ if (!deps.respawn) return;
6105
+ const wt = resumeCandidate.worktree_path;
6106
+ const worktreeExists = deps.worktreeExists ?? existsSync4;
6107
+ const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
6108
+ const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
6109
+ const mode = canContinue ? "continue" : "fresh";
6110
+ try {
6111
+ await deps.respawn(resumeCandidate, mode);
6112
+ deps.db.updateRun(resumeCandidate.id, { stop_reason: null });
6113
+ deps.db.addEvent(
6114
+ resumeCandidate.id,
6115
+ "resumed",
6116
+ mode === "continue" ? "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (--continue: \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin + \u0441\u0435\u0441\u0441\u0438\u044F \u0446\u0435\u043B\u044B)" : "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, \u0441\u0442\u0430\u0440\u0442 \u0437\u0430\u043D\u043E\u0432\u043E (\u0441\u0435\u0441\u0441\u0438\u044F/worktree \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B)"
6117
+ );
6118
+ narrate(deps, `\u041F\u043E\u043B\u0435\u0433\u0447\u0430\u043B\u043E \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E ${droneLabel(resumeCandidate)} \u0438\u0437 \u043F\u0430\u0443\u0437\u044B.`);
6119
+ state.lastRampAt = now;
6120
+ } catch (e) {
6121
+ deps.db.updateRun(resumeCandidate.id, { status: "stalled" });
6122
+ deps.db.addEvent(resumeCandidate.id, "stalled", `\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0438\u0437 \u0440\u0435\u0441\u0443\u0440\u0441-\u043F\u0430\u0443\u0437\u044B \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
4175
6123
  }
6124
+ return;
6125
+ }
6126
+ for (const run of deps.db.listRuns("queued")) {
6127
+ const outcome = await trySpawnQueuedRun(deps, run);
6128
+ if (outcome === "skipped" || outcome === "closed") continue;
6129
+ state.lastRampAt = now;
6130
+ return;
4176
6131
  }
4177
6132
  }
4178
6133
  async function tickOnce(deps) {
4179
6134
  const now = deps.now ? deps.now() : Date.now();
4180
- const worktreeExists = deps.worktreeExists ?? existsSync3;
6135
+ const worktreeExists = deps.worktreeExists ?? existsSync4;
4181
6136
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
6137
+ if (deps.rehydrate) {
6138
+ const rh = deps.rehydrate;
6139
+ const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
6140
+ if (sinceLast >= rh.intervalMs && rh.ready()) {
6141
+ rh.lastAt = now;
6142
+ try {
6143
+ await rh.run();
6144
+ } catch (e) {
6145
+ console.error("[overmind] re-hydrate \u0437\u0435\u0440\u043A\u0430\u043B\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
6146
+ }
6147
+ }
6148
+ }
6149
+ if (deps.heartbeat) {
6150
+ const hb = deps.heartbeat;
6151
+ const sinceLast = hb.lastAt == null ? Infinity : now - hb.lastAt;
6152
+ if (sinceLast >= hb.intervalMs) {
6153
+ hb.lastAt = now;
6154
+ await hb.push();
6155
+ }
6156
+ }
6157
+ if (deps.rehydrate && deps.kill) {
6158
+ const kill = deps.kill;
6159
+ for (const run of deps.db.listRuns()) {
6160
+ if (!isTerminalStatus(run.status) || run.pid == null || !deps.isAlive(run.pid)) continue;
6161
+ console.log(`[overmind] reap: \u043F\u0440\u043E\u0433\u043E\u043D ${run.name} (#${run.id}) \u043F\u043E\u043C\u0435\u0447\u0435\u043D ${run.status} \u0438\u0437\u0432\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 pid ${run.pid}`);
6162
+ try {
6163
+ kill(run.pid);
6164
+ } catch {
6165
+ }
6166
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
6167
+ });
6168
+ }
6169
+ }
6170
+ if (deps.sweep) {
6171
+ const sw = deps.sweep;
6172
+ const sinceLast = sw.lastAt == null ? Infinity : now - sw.lastAt;
6173
+ if (sinceLast >= sw.intervalMs) {
6174
+ sw.lastAt = now;
6175
+ try {
6176
+ await sw.run();
6177
+ } catch (e) {
6178
+ console.error("[overmind] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
6179
+ }
6180
+ }
6181
+ }
4182
6182
  let stormActive = false;
4183
6183
  if (deps.storm) {
4184
6184
  for (const run of deps.db.listRuns("running")) {
4185
- if (run.resolves_run_id == null && run.pid != null && !deps.isAlive(run.pid)) {
6185
+ if (run.resolves_run_id == null && run.reviews_run_id == null && run.pid != null && !deps.isAlive(run.pid)) {
4186
6186
  deps.db.recordDeath(run.id, `\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 (pid ${run.pid} \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442)`);
4187
6187
  }
4188
6188
  }
4189
6189
  stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
4190
6190
  }
4191
- if (!stormActive) {
4192
- for (const run of deps.db.listRuns("queued")) {
4193
- if (deps.db.unmetDeps(run.depends_on).length > 0) continue;
4194
- try {
4195
- await deps.spawn(run);
4196
- } catch (e) {
4197
- if (e instanceof AreaBusyError) continue;
4198
- deps.db.updateRun(run.id, { status: "failed" });
4199
- deps.db.addEvent(run.id, "failed", String(e));
4200
- }
4201
- }
6191
+ const droneCap = deps.maxDrones ? await deps.maxDrones() : Infinity;
6192
+ if (deps.resourcesOk) {
6193
+ await manageRelief(deps, now, droneCap, stormActive);
6194
+ } else if (!stormActive) {
6195
+ await batchSpawnQueue(deps, droneCap);
4202
6196
  }
4203
6197
  for (const run of deps.db.listRuns("review")) {
4204
6198
  try {
@@ -4210,20 +6204,31 @@ async function tickOnce(deps) {
4210
6204
  deps.db.addEvent(run.resolves_run_id, "conflict", `\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440\u043E\u043C ${run.name} \u2014 PR \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D`);
4211
6205
  continue;
4212
6206
  }
4213
- if (!run.pr_url) continue;
4214
- const st = await deps.prStatus(run.pr_url);
4215
- if (st.merged) {
4216
- await deps.finishTask(run.task_slug);
6207
+ if (run.reviews_run_id != null) {
4217
6208
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
4218
6209
  });
4219
- deps.db.addEvent(run.id, "merged", run.pr_url);
4220
6210
  deps.db.updateRun(run.id, { status: "done" });
4221
- deps.db.addEvent(run.id, "done");
6211
+ deps.db.addEvent(run.id, "done", "\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D");
6212
+ narrate(deps, `${droneLabel(run)} \u0432\u044B\u043D\u0435\u0441 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0440\u0435\u0432\u044C\u044E \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
6213
+ continue;
6214
+ }
6215
+ if (!run.pr_url) continue;
6216
+ const st = await deps.prStatus(run.pr_url);
6217
+ if (st.merged) {
6218
+ await finalizeMergedReview(deps, run);
6219
+ narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
4222
6220
  continue;
4223
6221
  }
4224
6222
  if (st.conflicting && deps.resolveConflict && !stormActive) {
6223
+ if (deps.resourcesOk && !deps.resourcesOk()) continue;
6224
+ if (countActiveDrones(deps.db) >= droneCap) continue;
4225
6225
  if (deps.db.activeResolverFor(run.id)) continue;
4226
- if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) continue;
6226
+ if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
6227
+ if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
6228
+ deps.db.addEvent(run.id, "conflict", `${RESOLVE_ESCALATION_MARK}: \u0430\u0432\u0442\u043E-\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0441\u044F \u0437\u0430 ${RESOLVE_LIMIT} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
6229
+ }
6230
+ continue;
6231
+ }
4227
6232
  const last = lastConflictAttemptMs(deps.db, run.id);
4228
6233
  if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) continue;
4229
6234
  deps.db.addEvent(run.id, "conflict", "PR \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 main \u2014 \u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 main (merge-main-in)");
@@ -4232,15 +6237,75 @@ async function tickOnce(deps) {
4232
6237
  } catch (e) {
4233
6238
  deps.db.addEvent(run.id, "conflict", `merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
4234
6239
  }
6240
+ } else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
6241
+ await handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap);
6242
+ }
6243
+ if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0) {
6244
+ const attempts = deps.db.reviewerAttemptCountFor(run.id);
6245
+ let verdictFromComment = null;
6246
+ if (attempts > 0 && deps.verdictComment) {
6247
+ const lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
6248
+ verdictFromComment = await deps.verdictComment(run.pr_url, lastAttempt?.created_at ?? run.created_at).catch(() => null);
6249
+ }
6250
+ if (verdictFromComment) {
6251
+ const verdictText = verdictLabel(verdictFromComment.verdict);
6252
+ deps.db.updateRun(run.id, { review_verdict: verdictFromComment.verdict });
6253
+ deps.db.addEvent(run.id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430 (done() \u0431\u044B\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0443): ${verdictText}`);
6254
+ narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0430\u0448\u0451\u043B\u0441\u044F \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0445 PR (${verdictText}), \u0440\u0435\u0441\u043F\u0430\u0432\u043D \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
6255
+ } else if (attempts >= REVIEW_SPAWN_LIMIT) {
6256
+ if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
6257
+ deps.db.addEvent(run.id, "step", `${REVIEW_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
6258
+ narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F \u0437\u0430 ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
6259
+ }
6260
+ } else {
6261
+ const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
6262
+ const leavesSlotForDrones = countActiveDrones(deps.db) < droneCap - 1;
6263
+ if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk && leavesSlotForDrones) {
6264
+ const freshSt = await deps.prStatus(run.pr_url);
6265
+ if (freshSt.merged) {
6266
+ await finalizeMergedReview(deps, run);
6267
+ narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E, \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
6268
+ } else if (freshSt.state === "CLOSED") {
6269
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
6270
+ });
6271
+ deps.db.updateRun(run.id, { status: "done" });
6272
+ deps.db.addEvent(run.id, "done", "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
6273
+ narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
6274
+ } else if (freshSt.conflicting || freshSt.isDraft) {
6275
+ deps.db.addEvent(run.id, "step", "PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430");
6276
+ } else {
6277
+ try {
6278
+ await deps.spawnReviewer(run, st.files ?? []);
6279
+ narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
6280
+ } catch (e) {
6281
+ deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
6282
+ }
6283
+ }
6284
+ }
6285
+ }
4235
6286
  }
4236
6287
  } catch (e) {
4237
6288
  console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B review-\u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
4238
6289
  }
4239
6290
  }
6291
+ await closeMergedTaskRuns(deps);
6292
+ let liveChildPpids;
6293
+ try {
6294
+ liveChildPpids = deps.livePpids?.() ?? null;
6295
+ } catch (e) {
6296
+ liveChildPpids = null;
6297
+ logTransientSpawnError("ps (livePpids)", e);
6298
+ }
4240
6299
  for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
4241
- if (run.pid == null) continue;
4242
- const alive = deps.isAlive(run.pid);
4243
- if (stormActive && run.resolves_run_id == null) {
6300
+ const alive = run.pid != null && deps.isAlive(run.pid);
6301
+ if (!alive && deps.accountCost) {
6302
+ try {
6303
+ deps.accountCost(run);
6304
+ } catch (e) {
6305
+ console.error(`[overmind] cost-\u0443\u0447\u0451\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
6306
+ }
6307
+ }
6308
+ if (stormActive && run.resolves_run_id == null && run.reviews_run_id == null) {
4244
6309
  if (!alive) {
4245
6310
  deps.db.updateRun(run.id, { status: "storm_dead" });
4246
6311
  deps.db.addEvent(run.id, "storm_dead", STORM_DEAD_TEXT);
@@ -4272,7 +6337,7 @@ async function tickOnce(deps) {
4272
6337
  }
4273
6338
  continue;
4274
6339
  }
4275
- if (!alive && run.status === "running" && run.resolves_run_id == null && !run.pr_url && deps.openPrsForBranch) {
6340
+ if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && !run.pr_url && deps.openPrsForBranch) {
4276
6341
  const branch = run.branch || run.task_slug;
4277
6342
  const prs = await deps.openPrsForBranch(branch, run.repo).catch(() => []);
4278
6343
  if (prs.length > 0) {
@@ -4283,7 +6348,7 @@ async function tickOnce(deps) {
4283
6348
  continue;
4284
6349
  }
4285
6350
  }
4286
- if (!alive && run.status === "running" && run.resolves_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
6351
+ if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
4287
6352
  const wt = run.worktree_path;
4288
6353
  const wtAlive = !!wt && worktreeExists(wt);
4289
6354
  const canContinue = wtAlive && hasTranscript(wt);
@@ -4296,11 +6361,16 @@ async function tickOnce(deps) {
4296
6361
  "respawned",
4297
6362
  mode === "continue" ? `\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 #${n}: --continue (worktree \u0438 \u0442\u0440\u0430\u043D\u0441\u043A\u0440\u0438\u043F\u0442 \u0436\u0438\u0432\u044B, \u0441\u043F\u0430\u0441\u0430\u0435\u043C \u0440\u0430\u0431\u043E\u0442\u0443)` : `\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 #${n}: \u0447\u0438\u0441\u0442\u044B\u0439 \u0441\u0442\u0430\u0440\u0442 (\u043C\u043E\u043B\u043E\u0434\u0430\u044F \u0441\u043C\u0435\u0440\u0442\u044C <${Math.round(RESPAWN_YOUNG_MS / 1e3)}\u0441 \u2014 \u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E \u0432\u043E\u043F\u0440\u043E\u0441-\u043D\u0430-\u0441\u0442\u0430\u0440\u0442\u0435, \u0441\u0435\u0441\u0441\u0438\u044E \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u043C)`
4298
6363
  );
6364
+ narrate(
6365
+ deps,
6366
+ mode === "continue" ? `${droneLabel(run)} \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430, \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0442\u0435\u0440\u044F\u0435\u043C.` : `${droneLabel(run)} \u0443\u043C\u0435\u0440 \u043D\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 \u2014 \u043F\u0435\u0440\u0435\u0441\u043F\u0430\u0432\u043D\u0438\u0432\u0430\u044E \u0441 \u043D\u0443\u043B\u044F.`
6367
+ );
4299
6368
  try {
4300
6369
  await deps.respawn(run, mode);
4301
6370
  } catch (e) {
4302
6371
  deps.db.updateRun(run.id, { status: "stalled" });
4303
6372
  deps.db.addEvent(run.id, "stalled", `\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C: ${String(e)}`);
6373
+ narrate(deps, `\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 ${droneLabel(run)} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u0434\u0430\u044E\u0441\u044C, \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0437\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C.`);
4304
6374
  }
4305
6375
  continue;
4306
6376
  }
@@ -4308,8 +6378,10 @@ async function tickOnce(deps) {
4308
6378
  let reason = null;
4309
6379
  if (!alive) {
4310
6380
  reason = run.respawns >= RESPAWN_LIMIT ? `\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440: \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u043B\u0438\u043C\u0438\u0442 \u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0439 (${RESPAWN_LIMIT})` : "\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440: pid \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442";
4311
- } else if (run.status === "running" && now - await livenessMs(deps, run) > STALL_MS) {
6381
+ narrate(deps, `${droneLabel(run)} \u043D\u0435 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0432 \u0441\u0435\u0431\u044F \u2014 \u0441\u0434\u0430\u044E\u0441\u044C, \u0434\u0430\u043B\u044C\u0448\u0435 \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`);
6382
+ } else if (run.status === "running" && now - await livenessMs(deps, run, now, liveChildPpids) > STALL_MS) {
4312
6383
  reason = `\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441: \u043C\u043E\u043B\u0447\u0438\u0442 > ${Math.round(STALL_MS / 6e4)} \u043C\u0438\u043D`;
6384
+ narrate(deps, `${droneLabel(run)} \u0437\u0430\u0442\u0438\u0445 \u0434\u043E\u043B\u044C\u0448\u0435 ${Math.round(STALL_MS / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u043E\u043C\u0435\u0447\u0430\u044E \u0437\u0430\u0432\u0438\u0441\u0448\u0438\u043C, \u0433\u043B\u044F\u043D\u0443, \u0447\u0442\u043E \u0442\u0430\u043C.`);
4313
6385
  }
4314
6386
  if (!reason) continue;
4315
6387
  deps.db.updateRun(run.id, { status: "stalled" });
@@ -4324,6 +6396,8 @@ async function tickOnce(deps) {
4324
6396
  if (deps.respawn && !stormActive) {
4325
6397
  for (const run of deps.db.listRuns("paused")) {
4326
6398
  if (run.kind === "hero") continue;
6399
+ if (run.reviews_run_id != null) continue;
6400
+ if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
4327
6401
  if (run.pid != null && deps.isAlive(run.pid)) continue;
4328
6402
  const wt = run.worktree_path;
4329
6403
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -4341,10 +6415,42 @@ async function tickOnce(deps) {
4341
6415
  }
4342
6416
  }
4343
6417
  }
4344
- if (deps.intake && !stormActive && deps.intake.maxInFlight > 0) {
6418
+ if (deps.anomalies) {
6419
+ const az = deps.anomalies;
6420
+ let eligible = [];
6421
+ try {
6422
+ const candidates = buildAnomalyCandidates(deps.db, now, az.thresholds, {
6423
+ autoReviewEnabled: az.autoReviewEnabled,
6424
+ ...az.outboxPendingCount ? { outboxPendingCount: az.outboxPendingCount() } : {}
6425
+ });
6426
+ eligible = az.tracker.eligible(now, candidates);
6427
+ } catch (e) {
6428
+ console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0434\u0435\u0442\u0435\u043A\u0442 \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
6429
+ }
6430
+ for (const candidate of eligible) {
6431
+ const signal = { key: candidate.key, ...candidate.build() };
6432
+ try {
6433
+ await az.notify(signal);
6434
+ } catch (e) {
6435
+ console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
6436
+ continue;
6437
+ }
6438
+ az.tracker.commit(candidate.key, now);
6439
+ if (signal.runId != null) {
6440
+ try {
6441
+ deps.db.addEvent(signal.runId, "anomaly", signal.body);
6442
+ } catch (e) {
6443
+ console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0432\u0435\u0445\u0430 \xAB${signal.key}\xBB \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u0430\u0441\u044C:`, e);
6444
+ }
6445
+ }
6446
+ narrate(deps, `\u26A0\uFE0F ${signal.title}`);
6447
+ }
6448
+ }
6449
+ if (deps.intake && !stormActive && deps.intake.maxInFlight > 0 && (!deps.resourcesOk || deps.resourcesOk())) {
4345
6450
  const it = deps.intake;
6451
+ const claimCap = deps.maxDrones ? droneCap : it.maxInFlight;
4346
6452
  const sinceLastClaim = it.lastClaimAt == null ? Infinity : now - it.lastClaimAt;
4347
- if (sinceLastClaim >= it.intervalMs && countInFlight(deps.db) < it.maxInFlight) {
6453
+ if (sinceLastClaim >= it.intervalMs && countInFlight(deps.db) < claimCap) {
4348
6454
  it.lastClaimAt = now;
4349
6455
  try {
4350
6456
  await it.claim();
@@ -4427,45 +6533,140 @@ async function resolveConflict(deps, reviewRun) {
4427
6533
  await deps.git.removeWorktree(repo, wt).catch(() => {
4428
6534
  });
4429
6535
  deps.db.updateRun(resolver.id, { status: "failed" });
4430
- deps.db.addEvent(resolver.id, "failed", `merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
6536
+ const marker = isTransientResourceError(e) ? "\u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C git-\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (\u0440\u0435\u0441\u0443\u0440\u0441, \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442)" : "merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F";
6537
+ deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
6538
+ throw e;
6539
+ }
6540
+ }
6541
+ async function spawnReviewer(deps, reviewRun, files) {
6542
+ const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
6543
+ const branch = reviewRun.branch || reviewRun.task_slug;
6544
+ const checklist = buildReviewChecklist(reviewRun, files);
6545
+ const reviewer = deps.db.createRun({
6546
+ task_slug: reviewRun.task_slug,
6547
+ repo,
6548
+ kind: "reviewer",
6549
+ reviews_run_id: reviewRun.id
6550
+ });
6551
+ const wt = join7(deps.worktreesDir, basename3(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
6552
+ try {
6553
+ await deps.git.prepareDetachedWorktree(repo, branch, wt);
6554
+ await deps.git.setWorktreeIdentity(wt, reviewer.name);
6555
+ const prompt = buildReviewerPrompt(reviewRun, reviewer, checklist, branch);
6556
+ const pid = deps.launch({ worktree: wt, run: reviewer, prompt });
6557
+ deps.db.updateRun(reviewer.id, { status: "running", worktree_path: wt, branch, pid });
6558
+ deps.db.addEvent(reviewRun.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E: \u043F\u043E\u0434\u043D\u044F\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${reviewer.name} (#${reviewer.id})`);
6559
+ return deps.db.getRun(reviewer.id);
6560
+ } catch (e) {
6561
+ await deps.git.removeWorktree(repo, wt).catch(() => {
6562
+ });
6563
+ deps.db.updateRun(reviewer.id, { status: "failed" });
6564
+ deps.db.addEvent(reviewer.id, "failed", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
4431
6565
  throw e;
4432
6566
  }
4433
6567
  }
4434
- async function respawnRun(deps, run, mode) {
6568
+ async function respawnRun(deps, run, mode, nudge) {
4435
6569
  const wt = run.worktree_path;
4436
6570
  if (!wt) throw new Error(`\u043D\u0435\u0442 worktree_path \u0434\u043B\u044F \u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}`);
4437
6571
  const repo = normalizeRepo(run.repo || deps.repoFallback);
4438
6572
  if (mode === "continue") {
4439
- const nudge = run.kind === "hero" ? HERO_CONTINUE_NUDGE : CONTINUE_NUDGE;
4440
- const pid2 = deps.launch({ worktree: wt, run, prompt: nudge, continueSession: true });
6573
+ const prompt2 = nudge ?? (run.kind === "hero" ? HERO_CONTINUE_NUDGE : CONTINUE_NUDGE);
6574
+ const pid2 = deps.launch({ worktree: wt, run, prompt: prompt2, continueSession: true });
4441
6575
  deps.db.updateRun(run.id, { status: "running", pid: pid2 });
4442
6576
  return;
4443
6577
  }
4444
- if (!existsSync3(wt)) {
6578
+ if (!existsSync4(wt)) {
4445
6579
  const branch = run.branch || run.task_slug;
4446
6580
  const source = await addWorktree(repo, wt, branch);
4447
6581
  await setWorktreeIdentity(wt, run.name);
4448
6582
  const sourceMsg = worktreeSourceMessage(branch, source);
4449
6583
  if (sourceMsg) deps.db.addEvent(run.id, "step", sourceMsg);
4450
6584
  }
4451
- const task = await deps.getTask(run.task_slug);
4452
- const prompt = run.kind === "hero" ? buildHeroPrompt(task, run) : buildPrompt(task, run);
6585
+ const [task, verifyMode] = await Promise.all([
6586
+ deps.getTask(run.task_slug),
6587
+ deps.verifyMode ? deps.verifyMode(repo) : Promise.resolve(void 0)
6588
+ ]);
6589
+ const taskContext = task && deps.getTaskContext ? await deps.getTaskContext(task) : void 0;
6590
+ const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
4453
6591
  const pid = deps.launch({ worktree: wt, run, prompt });
4454
6592
  deps.db.updateRun(run.id, { status: "running", pid });
4455
6593
  }
6594
+ async function syncCrashedOnStartup(deps) {
6595
+ let orphaned = 0;
6596
+ let merged = 0;
6597
+ for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
6598
+ if (run.pid != null) continue;
6599
+ if (run.pr_url && deps.prMerged) {
6600
+ const isMerged = await deps.prMerged(run.pr_url).catch(() => false);
6601
+ if (isMerged) {
6602
+ if (deps.finishTask) await deps.finishTask(run.task_slug).catch(() => {
6603
+ });
6604
+ deps.db.updateRun(run.id, { status: "done" });
6605
+ deps.db.addEvent(run.id, "done", "PR \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u043F\u043E\u0441\u043B\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 (TASWM-128)");
6606
+ merged++;
6607
+ continue;
6608
+ }
6609
+ }
6610
+ deps.db.updateRun(run.id, { status: "stalled" });
6611
+ deps.db.addEvent(run.id, "stalled", "\u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B \u043F\u043E\u0441\u043B\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 (\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u043D\u0435\u0442) \u2014 \u0436\u0434\u0451\u0442 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (swarm wake)");
6612
+ orphaned++;
6613
+ }
6614
+ return { orphaned, merged };
6615
+ }
4456
6616
  async function makeStore(repo) {
4457
- if (SWARM_BACKEND !== "client") return new SwarmDb(DB_PATH, repo);
6617
+ if (SWARM_BACKEND !== "client") return { store: new SwarmDb(DB_PATH, repo), reconciled: true };
4458
6618
  const failing = async () => ({ isError: true });
4459
- const call = await connectSwarmMcp() ?? failing;
4460
- const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo });
6619
+ const [call, repoRegistry] = await Promise.all([
6620
+ connectSwarmMcp().then((c) => c ?? failing),
6621
+ buildRepoRegistry(SWARM_REPO_PATHS)
6622
+ ]);
6623
+ if (SWARM_REPO_PATHS.length > 0 && repoRegistry.length < SWARM_REPO_PATHS.length) {
6624
+ console.warn(
6625
+ `[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${repoRegistry.length}/${SWARM_REPO_PATHS.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
6626
+ );
6627
+ }
6628
+ const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry });
4461
6629
  await client.hydrate();
4462
- console.log(`overmind backend=client (\u0442\u043E\u043D\u043A\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442, outbox=${SWARM_OUTBOX})`);
4463
- return client;
6630
+ const rec = client.reconcileRuntime({ worktreesDir: WORKTREES_DIR, listProcesses: listDroneProcesses });
6631
+ if (rec.reconciled) {
6632
+ console.log(`overmind backend=client (\u0442\u043E\u043D\u043A\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442, outbox=${SWARM_OUTBOX}); \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B: \u0436\u0438\u0432\u044B\u0445 ${rec.alive}, \u043C\u0451\u0440\u0442\u0432\u044B\u0445 ${rec.dead}`);
6633
+ } else {
6634
+ console.warn(
6635
+ `overmind backend=client (\u0442\u043E\u043D\u043A\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442, outbox=${SWARM_OUTBOX}); \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F (ps \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D) \u2014 running/blocked \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0435\u0440\u0435\u043A\u043E\u043D\u0441\u0438\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u044B, startup-sync (TASWM-128) \u0431\u0443\u0434\u0435\u0442 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D`
6636
+ );
6637
+ }
6638
+ return { store: client, reconciled: rec.reconciled };
6639
+ }
6640
+ var crashGuardsInstalled = false;
6641
+ function installCrashGuards() {
6642
+ if (crashGuardsInstalled) return;
6643
+ crashGuardsInstalled = true;
6644
+ process.on("uncaughtException", (e) => {
6645
+ console.error("[overmind] uncaughtException \u2014 \u0434\u0435\u043C\u043E\u043D \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u0440\u0430\u0431\u043E\u0442\u0443:", e);
6646
+ });
6647
+ process.on("unhandledRejection", (e) => {
6648
+ console.error("[overmind] unhandledRejection \u2014 \u0434\u0435\u043C\u043E\u043D \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u0440\u0430\u0431\u043E\u0442\u0443:", e);
6649
+ });
4464
6650
  }
4465
6651
  async function runDaemon(opts) {
4466
- const db = await makeStore(opts.repo);
6652
+ installCrashGuards();
6653
+ const { store: db, reconciled } = await makeStore(opts.repo);
4467
6654
  const tl = await connectTaskless();
4468
- const launch = makeLauncher({ overmindEntry: THIS_FILE, dbPath: DB_PATH });
6655
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
6656
+ if (reconciled) {
6657
+ const sync = await syncCrashedOnStartup({
6658
+ db,
6659
+ prMerged: (url) => prStatus(url).then((s) => s.merged),
6660
+ finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve()
6661
+ });
6662
+ if (sync.orphaned || sync.merged) {
6663
+ console.log(`[overmind] startup-sync (TASWM-128): \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u043E ${sync.orphaned} \u2192 stalled, \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E ${sync.merged} \u2192 done`);
6664
+ }
6665
+ } else {
6666
+ console.warn("[overmind] startup-sync (TASWM-128) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D \u2014 \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0440\u0430\u043D\u0442\u0430\u0439\u043C\u0430 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F, \u0436\u0438\u0432\u044B\u0445 \u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438\u043C \u043F\u043E \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E\u0441\u0442\u0438");
6667
+ }
6668
+ }
6669
+ const launch = makeLauncher({ overmindEntry: THIS_FILE, dbPath: DB_PATH, runLogsDir: RUN_LOGS_DIR });
4469
6670
  const baseSpawn = {
4470
6671
  db,
4471
6672
  repo: opts.repo,
@@ -4478,25 +6679,141 @@ async function runDaemon(opts) {
4478
6679
  const client = db;
4479
6680
  intake = { claim: () => client.claim(), maxInFlight: SWARM_MAX_IN_FLIGHT, intervalMs: SWARM_CLAIM_INTERVAL_MS };
4480
6681
  }
6682
+ let rehydrate;
6683
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_REHYDRATE_MS > 0) {
6684
+ const client = db;
6685
+ rehydrate = { run: () => client.hydrate(), ready: () => client.pendingCount() === 0, intervalMs: SWARM_REHYDRATE_MS };
6686
+ }
6687
+ let heartbeat;
6688
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_PROD_HEARTBEAT_MS > 0) {
6689
+ const client = db;
6690
+ heartbeat = { push: () => client.heartbeat(), intervalMs: SWARM_PROD_HEARTBEAT_MS };
6691
+ }
6692
+ let anomalies;
6693
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
6694
+ const client = db;
6695
+ anomalies = {
6696
+ tracker: new AnomalyTracker(SWARM_ANOMALY_RENOTIFY_MS),
6697
+ thresholds: {
6698
+ failedSpikeCount: SWARM_ANOMALY_FAILED_SPIKE_COUNT,
6699
+ failedSpikeWindowMs: SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
6700
+ queueStalledMs: SWARM_ANOMALY_QUEUE_STALLED_MS,
6701
+ prStuckMergeMs: SWARM_ANOMALY_PR_STUCK_MERGE_MS,
6702
+ prNoReviewerMs: SWARM_ANOMALY_PR_NO_REVIEWER_MS,
6703
+ questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
6704
+ outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS
6705
+ },
6706
+ autoReviewEnabled: SWARM_AUTO_REVIEW,
6707
+ outboxPendingCount: () => client.pendingCount(),
6708
+ notify: (signal) => client.notifyAnomaly(signal)
6709
+ };
6710
+ }
6711
+ let maxDrones;
6712
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
6713
+ const client = db;
6714
+ maxDrones = makeMaxDronesReader({
6715
+ fetch: () => client.ownerMaxDrones(),
6716
+ fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
6717
+ ttlMs: 1e4
6718
+ });
6719
+ }
6720
+ let verifyModeReader;
6721
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
6722
+ const client = db;
6723
+ verifyModeReader = makeVerifyModeReader({
6724
+ fetch: async (repo) => {
6725
+ const repoFullName = await remoteRepoFullName(repo).catch(() => null);
6726
+ return repoFullName ? client.repoVerifyMode(repoFullName) : null;
6727
+ },
6728
+ fallback: SWARM_VERIFY,
6729
+ ttlMs: 6e4
6730
+ });
6731
+ }
6732
+ let spawnReviewerFn;
6733
+ let verdictCommentFn;
6734
+ let reviewCutoffAtValue;
6735
+ if (SWARM_AUTO_REVIEW) {
6736
+ spawnReviewerFn = (reviewRun, files) => spawnReviewer(
6737
+ {
6738
+ db,
6739
+ repoFallback: opts.repo,
6740
+ worktreesDir: WORKTREES_DIR,
6741
+ launch,
6742
+ git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree }
6743
+ },
6744
+ reviewRun,
6745
+ files
6746
+ ).then(() => {
6747
+ });
6748
+ verdictCommentFn = (prUrl, sinceIso) => latestVerdictComment(prUrl, sinceIso);
6749
+ reviewCutoffAtValue = reviewCutoffAt();
6750
+ }
6751
+ const narrator = makeOvermindNarrator();
4481
6752
  const deps = {
4482
6753
  db,
6754
+ narrate: narrator.narrate,
4483
6755
  spawn: async (run) => {
4484
- const task = tl ? await tl.getTask(run.task_slug) : null;
4485
- const prompt = run.kind === "hero" ? buildHeroPrompt(task, run) : buildPrompt(task, run);
6756
+ const [task, verifyMode] = await Promise.all([
6757
+ tl ? tl.getTask(run.task_slug) : Promise.resolve(null),
6758
+ verifyModeReader ? verifyModeReader(normalizeRepo(run.repo || opts.repo)) : Promise.resolve(void 0)
6759
+ ]);
6760
+ const taskContext = task && tl ? await fetchTaskContext(tl, task) : void 0;
6761
+ const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
4486
6762
  await spawnDrone({ ...baseSpawn, buildPrompt: () => prompt }, run);
4487
6763
  if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
4488
6764
  if (tl) await tl.startTask(run.task_slug);
4489
6765
  },
4490
- prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting })),
6766
+ prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
4491
6767
  // Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
4492
6768
  // (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
4493
6769
  openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
6770
+ // Смерженный PR ветки-слага (TASWM-206) — тот же фолбэк на дефолт-репо роя, что и
6771
+ // openPrsForBranch выше; реальный gh, логика закрытия дублей — в tickOnce (тестируется).
6772
+ mergedPrForBranch: (branch, repoDir) => mergedPrForBranch(branch, repoDir || opts.repo),
4494
6773
  finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve(),
4495
6774
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
4496
6775
  isAlive: pidAlive,
6776
+ // Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
6777
+ accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
6778
+ // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
6779
+ livePpids,
6780
+ // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
6781
+ // POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
6782
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
6783
+ // Защитный sweep осиротевших процессов/docker-контейнеров (TASWM-210): активные
6784
+ // worktree-пути и docker-префиксы — из ВСЕХ ранов в БД (не только running/blocked —
6785
+ // paused/stalled/queued/review/storm_dead тоже держат свой worktree/контейнеры, их
6786
+ // сносить рано), терминальные (done/failed/stopped/superseded) — кандидаты на снос.
6787
+ sweep: {
6788
+ run: () => {
6789
+ const runsWithWorktree = db.listRuns().filter((r) => r.worktree_path);
6790
+ return sweepOrphans({
6791
+ worktreesDir: WORKTREES_DIR,
6792
+ activeWorktreePaths: new Set(runsWithWorktree.filter((r) => !isTerminalStatus(r.status)).map((r) => r.worktree_path)),
6793
+ containerRunPrefixes: runsWithWorktree.map((r) => ({
6794
+ prefix: composeProjectPrefix(basename3(r.worktree_path)),
6795
+ active: !isTerminalStatus(r.status)
6796
+ }))
6797
+ }).then(() => {
6798
+ });
6799
+ },
6800
+ intervalMs: SWARM_SWEEP_INTERVAL_MS,
6801
+ lastAt: null
6802
+ },
4497
6803
  isDirty: (p) => worktreeDirty(p),
4498
6804
  // git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
4499
6805
  lastCommitMs: (p) => worktreeLastCommitMs(p),
6806
+ // mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
6807
+ // на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
6808
+ transcriptMtimeMs: (p) => transcriptMtimeMs(p),
6809
+ // Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
6810
+ resourcesOk: hostResourcesOk,
6811
+ // Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
6812
+ // хоста (общий кэш в resources.ts), но заведомо строже настроенные пороги.
6813
+ reliefNeeded: hostReliefNeeded,
6814
+ // Состояние градуального контроллера (окна ramp/pause, reliefSince) — заводим ОДИН раз
6815
+ // на цикл демона, tickOnce (manageRelief) мутирует эти же поля каждый тик.
6816
+ relief: {},
4500
6817
  // merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
4501
6818
  // фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
4502
6819
  resolveConflict: (reviewRun) => resolveConflict(
@@ -4512,13 +6829,30 @@ async function runDaemon(opts) {
4512
6829
  }),
4513
6830
  // Авто-оживление / пробуждение прогона в его же worktree — общий примитив respawnRun
4514
6831
  // (kind-aware: герой ← HERO_CONTINUE_NUDGE/buildHeroPrompt). Лимит — RESPAWN_LIMIT, считает tickOnce.
4515
- respawn: (run, mode) => respawnRun(
4516
- { db, launch, repoFallback: opts.repo, worktreesDir: WORKTREES_DIR, getTask: (slug) => tl ? tl.getTask(slug) : Promise.resolve(null) },
6832
+ // nudge прокинут для CI-побудки (TASWM-102): tickOnce передаёт buildCiWakeNudge(summary).
6833
+ respawn: (run, mode, nudge) => respawnRun(
6834
+ {
6835
+ db,
6836
+ launch,
6837
+ repoFallback: opts.repo,
6838
+ worktreesDir: WORKTREES_DIR,
6839
+ getTask: (slug) => tl ? tl.getTask(slug) : Promise.resolve(null),
6840
+ ...verifyModeReader ? { verifyMode: verifyModeReader } : {},
6841
+ getTaskContext: (task) => tl ? fetchTaskContext(tl, task) : Promise.resolve(void 0)
6842
+ },
4517
6843
  run,
4518
- mode
6844
+ mode,
6845
+ nudge
4519
6846
  ),
4520
- worktreeExists: (p) => existsSync3(p),
6847
+ worktreeExists: (p) => existsSync4(p),
4521
6848
  hasTranscript: (p) => sessionTranscriptExists(p),
6849
+ // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
6850
+ // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
6851
+ // строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
6852
+ ci: {
6853
+ checks: (url) => prCiStatus(url),
6854
+ failureSummary: (failing, failingRunIds) => ciFailureSummary(failing, failingRunIds)
6855
+ },
4522
6856
  // Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
4523
6857
  // сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
4524
6858
  storm: {
@@ -4527,8 +6861,20 @@ async function runDaemon(opts) {
4527
6861
  // заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
4528
6862
  ...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
4529
6863
  },
6864
+ // Живой потолок дронов (client-режим, TASWM-126): ключ есть только когда сконфигурен.
6865
+ ...maxDrones ? { maxDrones } : {},
4530
6866
  // Авто-интейк (client-режим): ключ есть только когда intake сконфигурен (exactOptionalPropertyTypes).
4531
- ...intake ? { intake } : {}
6867
+ ...intake ? { intake } : {},
6868
+ // Периодический re-hydrate зеркала (client-режим): ключ есть только когда сконфигурен.
6869
+ ...rehydrate ? { rehydrate } : {},
6870
+ // Пульс overmind в прод (client-режим, TASWM-129): ключ есть только когда сконфигурен.
6871
+ ...heartbeat ? { heartbeat } : {},
6872
+ // Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
6873
+ ...anomalies ? { anomalies } : {},
6874
+ // Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
6875
+ // spawnReviewerFn/verdictCommentFn/reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW)
6876
+ // блоке выше, поэтому один и тот же гейт на все три (без отдельных тернарных условий).
6877
+ ...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, verdictComment: verdictCommentFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
4532
6878
  };
4533
6879
  const web = startWeb({
4534
6880
  db,
@@ -4536,10 +6882,12 @@ async function runDaemon(opts) {
4536
6882
  // дефолт роя в whitelist repo для /spawn
4537
6883
  taskUrlBase: TASKLESS_TASK_URL_BASE,
4538
6884
  // фолбэк-база ссылки на задачу (прогоны без снимка task_url)
6885
+ narratorLog: narrator.log,
6886
+ // overmind narration-лог (TASWM-123) — тот же нарратор, что deps.narrate
4539
6887
  replay: (qid, chosen) => replayDecision(
4540
6888
  {
4541
6889
  db,
4542
- kill: (pid) => process.kill(pid, "SIGTERM"),
6890
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
4543
6891
  closePR: (u) => closePR(u),
4544
6892
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
4545
6893
  },
@@ -4551,7 +6899,7 @@ async function runDaemon(opts) {
4551
6899
  stop: (slug, reason) => cancelRun(
4552
6900
  {
4553
6901
  db,
4554
- kill: (pid) => process.kill(pid, "SIGTERM"),
6902
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
4555
6903
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
4556
6904
  },
4557
6905
  slug,
@@ -4565,6 +6913,10 @@ async function runDaemon(opts) {
4565
6913
  console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
4566
6914
  const interval = opts.intervalMs ?? 4e3;
4567
6915
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
6916
+ if (deps.sweep) {
6917
+ deps.sweep.lastAt = Date.now();
6918
+ await deps.sweep.run().catch((e) => console.error("[overmind] \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e));
6919
+ }
4568
6920
  for (; ; ) {
4569
6921
  try {
4570
6922
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
@@ -4593,22 +6945,38 @@ if (isEntrypoint) {
4593
6945
  }
4594
6946
  }
4595
6947
  export {
6948
+ CI_CHECK_COOLDOWN_MS,
6949
+ CI_WAKE_COOLDOWN_MS,
6950
+ CI_WAKE_ESCALATION_MARK,
6951
+ CI_WAKE_LIMIT,
4596
6952
  CONFLICT_RESOLVE_COOLDOWN_MS,
4597
6953
  CONTINUE_NUDGE,
4598
6954
  HEADLESS_BLOCK,
4599
6955
  HERO_CONTINUE_NUDGE,
4600
6956
  OVERMIND_ENTRY,
6957
+ RESOLVE_ESCALATION_MARK,
4601
6958
  RESOLVE_LIMIT,
4602
6959
  RESPAWN_LIMIT,
4603
6960
  RESPAWN_YOUNG_MS,
6961
+ REVIEW_ESCALATION_MARK,
6962
+ REVIEW_SPAWN_LIMIT,
4604
6963
  STALL_MS,
6964
+ buildCiWakeNudge,
4605
6965
  buildHeroPrompt,
4606
6966
  buildPrompt,
4607
6967
  buildResolverPrompt,
6968
+ buildReviewChecklist,
6969
+ buildReviewerPrompt,
4608
6970
  cancelRun,
6971
+ hasLiveDescendant,
6972
+ listDroneProcesses,
6973
+ livePpids,
6974
+ parseLivePpids,
4609
6975
  replayDecision,
4610
6976
  resolveConflict,
4611
6977
  respawnRun,
4612
6978
  runDaemon,
6979
+ spawnReviewer,
6980
+ syncCrashedOnStartup,
4613
6981
  tickOnce
4614
6982
  };