@taskless-app/swarm 0.1.1 → 0.2.0

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