@taskless-app/swarm 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +372 -37
- package/dist/overmind.js +1030 -155
- package/package.json +7 -5
package/dist/overmind.js
CHANGED
|
@@ -92,6 +92,17 @@ function buildSegments(created, events, laneEnd) {
|
|
|
92
92
|
push(e.at);
|
|
93
93
|
phase = "running";
|
|
94
94
|
break;
|
|
95
|
+
// TASWM-166: раньше эти два kind'а не двигали фазу — хвост дорожки
|
|
96
|
+
// дорисовывался ТЕКУЩЕЙ (до остановки) фазой, напр. "running" сплошным
|
|
97
|
+
// зелёным до самого конца — визуально неотличимо от реально идущего прогона.
|
|
98
|
+
case "stopped":
|
|
99
|
+
push(e.at);
|
|
100
|
+
phase = "stopped";
|
|
101
|
+
break;
|
|
102
|
+
case "superseded":
|
|
103
|
+
push(e.at);
|
|
104
|
+
phase = "superseded";
|
|
105
|
+
break;
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
push(laneEnd);
|
|
@@ -112,7 +123,7 @@ function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
|
|
|
112
123
|
case "blocked":
|
|
113
124
|
return { icon: "\u2753", text: openQ ? `\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430: ${openQ}` : "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430", tone: "blocked" };
|
|
114
125
|
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" };
|
|
126
|
+
return { icon: "\u{1F4A4}", text: stopReason || "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "paused" };
|
|
116
127
|
case "storm_dead":
|
|
117
128
|
return { icon: "\u{1F4A4}", text: "\u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u0448\u0442\u043E\u0440\u043C Anthropic, \u0436\u0434\u0451\u0442 \u043E\u0442\u0431\u043E\u044F", tone: "paused" };
|
|
118
129
|
case "review": {
|
|
@@ -126,7 +137,9 @@ function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
|
|
|
126
137
|
case "failed":
|
|
127
138
|
return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
|
|
128
139
|
case "stopped":
|
|
129
|
-
return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "
|
|
140
|
+
return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "stopped" };
|
|
141
|
+
case "superseded":
|
|
142
|
+
return { icon: "\u21BA", text: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442", tone: "superseded" };
|
|
130
143
|
case "done":
|
|
131
144
|
return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
|
|
132
145
|
default:
|
|
@@ -377,10 +390,15 @@ __export(config_exports, {
|
|
|
377
390
|
SWARM_OUTBOX: () => SWARM_OUTBOX,
|
|
378
391
|
SWARM_PROD_HEARTBEAT_MS: () => SWARM_PROD_HEARTBEAT_MS,
|
|
379
392
|
SWARM_REHYDRATE_MS: () => SWARM_REHYDRATE_MS,
|
|
393
|
+
SWARM_RELIEF_LOAD_FACTOR: () => SWARM_RELIEF_LOAD_FACTOR,
|
|
394
|
+
SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
|
|
395
|
+
SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
|
|
380
396
|
SWARM_REPO: () => SWARM_REPO,
|
|
397
|
+
SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
|
|
381
398
|
SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
|
|
382
399
|
SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
|
|
383
400
|
SWARM_STORM_URL: () => SWARM_STORM_URL,
|
|
401
|
+
SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
|
|
384
402
|
SWARM_VERIFY: () => SWARM_VERIFY,
|
|
385
403
|
TASKLESS_API: () => TASKLESS_API,
|
|
386
404
|
TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
|
|
@@ -444,7 +462,7 @@ function reviewCutoffAt() {
|
|
|
444
462
|
}
|
|
445
463
|
return at;
|
|
446
464
|
}
|
|
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;
|
|
465
|
+
var SWARM_HOME, DB_PATH, WORKTREES_DIR, RUN_LOGS_DIR, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_REHYDRATE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_SWEEP_INTERVAL_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_REPO_PATHS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS;
|
|
448
466
|
var init_config = __esm({
|
|
449
467
|
"src/config.ts"() {
|
|
450
468
|
"use strict";
|
|
@@ -462,11 +480,16 @@ var init_config = __esm({
|
|
|
462
480
|
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
463
481
|
SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
464
482
|
SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
483
|
+
SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
|
|
484
|
+
SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
|
|
485
|
+
SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
|
|
465
486
|
SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
466
487
|
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
488
|
+
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
467
489
|
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
468
490
|
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
469
491
|
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
492
|
+
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
470
493
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
471
494
|
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
472
495
|
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
@@ -607,7 +630,7 @@ var db_exports = {};
|
|
|
607
630
|
__export(db_exports, {
|
|
608
631
|
SwarmDb: () => SwarmDb
|
|
609
632
|
});
|
|
610
|
-
import
|
|
633
|
+
import { createRequire } from "module";
|
|
611
634
|
import { existsSync } from "fs";
|
|
612
635
|
import { resolve as resolve2 } from "path";
|
|
613
636
|
function rowToQuestion(row) {
|
|
@@ -658,6 +681,16 @@ function rowToRun(row) {
|
|
|
658
681
|
updated_at: row.updated_at
|
|
659
682
|
};
|
|
660
683
|
}
|
|
684
|
+
function loadBetterSqlite3() {
|
|
685
|
+
const require2 = createRequire(import.meta.url);
|
|
686
|
+
try {
|
|
687
|
+
return require2("better-sqlite3");
|
|
688
|
+
} catch (err) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`better-sqlite3 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. Sqlite-\u0440\u0435\u0436\u0438\u043C (SwarmDb) \u2014 \u0434\u0435\u0444\u043E\u043B\u0442 \u043C\u043E\u043D\u043E\u0440\u0435\u043F\u044B/\u0442\u0435\u0441\u0442\u043E\u0432; \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u0438 sqlite \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 optional dependency (npm i, \u0431\u0435\u0437 --omit=optional) \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_BACKEND=client. \u0418\u0441\u0445\u043E\u0434\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430: ${err.message}`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
661
694
|
var TERMINAL_SQL, RESOLVER_TERMINAL_SQL, SCHEMA, SwarmDb;
|
|
662
695
|
var init_db = __esm({
|
|
663
696
|
"src/db.ts"() {
|
|
@@ -733,9 +766,12 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
733
766
|
SwarmDb = class {
|
|
734
767
|
db;
|
|
735
768
|
defaultRepo;
|
|
769
|
+
databaseCtor;
|
|
770
|
+
// для importDb (второй Database, source)
|
|
736
771
|
constructor(path = "swarm.db", defaultRepo = "") {
|
|
737
772
|
this.defaultRepo = normalizeRepo(defaultRepo);
|
|
738
|
-
this.
|
|
773
|
+
this.databaseCtor = loadBetterSqlite3();
|
|
774
|
+
this.db = new this.databaseCtor(path);
|
|
739
775
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
740
776
|
this.db.exec(SCHEMA);
|
|
741
777
|
for (const alter of [
|
|
@@ -871,6 +907,33 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
871
907
|
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status NOT IN ('failed','stalled')").get(reviewRunId);
|
|
872
908
|
return row.n;
|
|
873
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
|
|
912
|
+
* ВСЕГО (любой статус, включая failed/stalled — специально в отличие от reviewerCountFor
|
|
913
|
+
* выше) когда-либо поднималось на review-прогон #reviewRunId. Зеркалит resolverCountFor —
|
|
914
|
+
* плоский потолок попыток. Именно это ловит карусель: reviewerCountFor исключает
|
|
915
|
+
* failed/stalled (чтобы ОДИН транзиентный сбой не блокировал ревью навсегда), но тем
|
|
916
|
+
* самым допускает бесконечный ретрай, если ревьюер стабильно умирает без done().
|
|
917
|
+
*/
|
|
918
|
+
reviewerAttemptCountFor(reviewRunId) {
|
|
919
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ?").get(reviewRunId);
|
|
920
|
+
return row.n;
|
|
921
|
+
}
|
|
922
|
+
/** Последняя (любой статус) попытка ревьюера на review-прогон #reviewRunId (TASWM-196) —
|
|
923
|
+
* см. store.ts:lastReviewerAttemptFor. */
|
|
924
|
+
lastReviewerAttemptFor(reviewRunId) {
|
|
925
|
+
const row = this.db.prepare("SELECT * FROM runs WHERE reviews_run_id = ? ORDER BY id DESC LIMIT 1").get(reviewRunId);
|
|
926
|
+
return row ? rowToRun(row) : null;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
|
|
930
|
+
* closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
|
|
931
|
+
* не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
|
|
932
|
+
*/
|
|
933
|
+
activeReviewersFor(reviewRunId) {
|
|
934
|
+
const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
|
|
935
|
+
return rows.map(rowToRun);
|
|
936
|
+
}
|
|
874
937
|
/** Последний живой (не терминальный) прогон по slug — для swarm cancel. */
|
|
875
938
|
activeRunForSlug(slug) {
|
|
876
939
|
const row = this.db.prepare(
|
|
@@ -931,7 +994,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
931
994
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
932
995
|
if (already) return { imported: false, runs: already.run_count };
|
|
933
996
|
if (!existsSync(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
934
|
-
const src = new
|
|
997
|
+
const src = new this.databaseCtor(abs);
|
|
935
998
|
try {
|
|
936
999
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
937
1000
|
const events = src.prepare("SELECT * FROM events ORDER BY id").all();
|
|
@@ -1368,9 +1431,14 @@ var init_workspace = __esm({
|
|
|
1368
1431
|
// src/taskless.ts
|
|
1369
1432
|
var taskless_exports = {};
|
|
1370
1433
|
__export(taskless_exports, {
|
|
1434
|
+
TASK_CONTEXT_MAX_COMMENTS: () => TASK_CONTEXT_MAX_COMMENTS,
|
|
1435
|
+
TASK_CONTEXT_MAX_COMMENT_CHARS: () => TASK_CONTEXT_MAX_COMMENT_CHARS,
|
|
1436
|
+
TASK_CONTEXT_MAX_DOCS: () => TASK_CONTEXT_MAX_DOCS,
|
|
1437
|
+
TASK_CONTEXT_MAX_DOC_CHARS: () => TASK_CONTEXT_MAX_DOC_CHARS,
|
|
1371
1438
|
connectSwarmMcp: () => connectSwarmMcp,
|
|
1372
1439
|
connectTaskless: () => connectTaskless,
|
|
1373
1440
|
extractText: () => extractText,
|
|
1441
|
+
fetchTaskContext: () => fetchTaskContext,
|
|
1374
1442
|
makeTasklessClient: () => makeTasklessClient
|
|
1375
1443
|
});
|
|
1376
1444
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -1399,12 +1467,24 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1399
1467
|
url: taskUrlBase ? `${taskUrlBase}/tasks/${slug}` : null,
|
|
1400
1468
|
parentId: task.parentId ?? null,
|
|
1401
1469
|
projectId: task.projectId ?? null,
|
|
1402
|
-
statusCategory: task.status?.category ?? null
|
|
1470
|
+
statusCategory: task.status?.category ?? null,
|
|
1471
|
+
comments: (task.comments ?? []).map((c) => ({
|
|
1472
|
+
author: c.author?.displayName ?? "\u2014",
|
|
1473
|
+
createdAt: (c.createdAt ?? "").slice(0, 16).replace("T", " "),
|
|
1474
|
+
text: extractText(c.body)
|
|
1475
|
+
})),
|
|
1476
|
+
documents: (task.documents ?? []).filter((d) => !!d.id).map((d) => ({ id: d.id, title: d.title ?? d.id, url: d.url ?? null }))
|
|
1403
1477
|
};
|
|
1404
1478
|
idCache.set(slug, body.id);
|
|
1405
1479
|
idCache.set(body.id, body.id);
|
|
1406
1480
|
return body;
|
|
1407
1481
|
}
|
|
1482
|
+
async function getDocument(id) {
|
|
1483
|
+
const res = await safeCall("get_document", { id });
|
|
1484
|
+
const doc = res?.structuredContent?.document ?? null;
|
|
1485
|
+
if (!doc) return null;
|
|
1486
|
+
return { title: doc.title ?? "", body: extractText(doc.body) };
|
|
1487
|
+
}
|
|
1408
1488
|
async function resolveId(idOrSlug) {
|
|
1409
1489
|
const cached = idCache.get(idOrSlug);
|
|
1410
1490
|
if (cached) return cached;
|
|
@@ -1434,15 +1514,18 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1434
1514
|
if (id) await safeCall("add_comment", { taskId: id, body });
|
|
1435
1515
|
},
|
|
1436
1516
|
backlogStatusId,
|
|
1517
|
+
getDocument,
|
|
1437
1518
|
createTask: async (input) => {
|
|
1519
|
+
if (!input.projectId) {
|
|
1520
|
+
throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
|
|
1521
|
+
}
|
|
1438
1522
|
const res = await safeCall("create_task", {
|
|
1439
1523
|
...input.parentId ? { parentId: input.parentId } : {},
|
|
1440
1524
|
title: input.title,
|
|
1441
1525
|
description: input.description,
|
|
1442
1526
|
...input.estimateHours != null ? { estimateHours: input.estimateHours } : {},
|
|
1443
1527
|
...input.statusId ? { statusId: input.statusId } : {},
|
|
1444
|
-
|
|
1445
|
-
...input.projectId ? { projectId: input.projectId } : {},
|
|
1528
|
+
projectId: input.projectId,
|
|
1446
1529
|
force: true
|
|
1447
1530
|
});
|
|
1448
1531
|
const task = res?.structuredContent?.task ?? null;
|
|
@@ -1454,6 +1537,44 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1454
1537
|
}
|
|
1455
1538
|
};
|
|
1456
1539
|
}
|
|
1540
|
+
async function fetchTaskContext(client, task) {
|
|
1541
|
+
const parts = [];
|
|
1542
|
+
const comments = task.comments ?? [];
|
|
1543
|
+
if (comments.length > 0) {
|
|
1544
|
+
const tail = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
|
|
1545
|
+
const skipped = comments.length - tail.length;
|
|
1546
|
+
const lines = tail.map((c) => {
|
|
1547
|
+
const cut = c.text.length > TASK_CONTEXT_MAX_COMMENT_CHARS;
|
|
1548
|
+
const text = cut ? `${c.text.slice(0, TASK_CONTEXT_MAX_COMMENT_CHARS)}\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)` : c.text;
|
|
1549
|
+
return `\u2014 [${c.createdAt} \xB7 ${c.author}] ${text}`;
|
|
1550
|
+
});
|
|
1551
|
+
const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
|
|
1552
|
+
parts.push([header, ...lines].join("\n"));
|
|
1553
|
+
}
|
|
1554
|
+
const documents = task.documents ?? [];
|
|
1555
|
+
if (documents.length > 0) {
|
|
1556
|
+
const head = documents.slice(0, TASK_CONTEXT_MAX_DOCS);
|
|
1557
|
+
const rest = documents.slice(TASK_CONTEXT_MAX_DOCS);
|
|
1558
|
+
const renderedHead = await Promise.all(
|
|
1559
|
+
head.map(async (d) => {
|
|
1560
|
+
const doc = await client.getDocument(d.id);
|
|
1561
|
+
if (!doc) return `### ${d.title}${d.url ? ` (${d.url})` : ""}
|
|
1562
|
+
(\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442)`;
|
|
1563
|
+
const cut = doc.body.length > TASK_CONTEXT_MAX_DOC_CHARS;
|
|
1564
|
+
const text = cut ? `${doc.body.slice(0, TASK_CONTEXT_MAX_DOC_CHARS)}\u2026` : doc.body;
|
|
1565
|
+
return `### ${d.title}${d.url ? ` (${d.url})` : ""}${cut ? " \u2014 \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435/get_document" : ""}
|
|
1566
|
+
${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|
1567
|
+
})
|
|
1568
|
+
);
|
|
1569
|
+
const restLines = rest.map((d) => `- ${d.title}${d.url ? ` (${d.url})` : ""} \u2014 \u0442\u0435\u043B\u043E \u043D\u0435 \u043F\u043E\u0434\u0442\u044F\u043D\u0443\u0442\u043E, \u0447\u0438\u0442\u0430\u0439 \u0447\u0435\u0440\u0435\u0437 get_document/read_task`);
|
|
1570
|
+
parts.push(
|
|
1571
|
+
[`\u041F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0435 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B (${documents.length}):`, ...renderedHead, ...restLines.length ? [restLines.join("\n")] : []].join(
|
|
1572
|
+
"\n\n"
|
|
1573
|
+
)
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
return parts.join("\n\n");
|
|
1577
|
+
}
|
|
1457
1578
|
function parseJsonContent(res) {
|
|
1458
1579
|
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
1459
1580
|
if (!text) return null;
|
|
@@ -1486,7 +1607,7 @@ async function connectTaskless(cfg) {
|
|
|
1486
1607
|
});
|
|
1487
1608
|
const client = new Client({ name: "taskless-swarm", version: "0.1.0" }, { capabilities: {} });
|
|
1488
1609
|
try {
|
|
1489
|
-
await client.connect(transport);
|
|
1610
|
+
await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
|
|
1490
1611
|
} catch (e) {
|
|
1491
1612
|
console.warn(`Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`);
|
|
1492
1613
|
return null;
|
|
@@ -1524,7 +1645,7 @@ async function connectSwarmMcp(cfg) {
|
|
|
1524
1645
|
}
|
|
1525
1646
|
return async (name, args) => await client.callTool({ name, arguments: { ...args, ...ws } });
|
|
1526
1647
|
}
|
|
1527
|
-
var SLUG_RE;
|
|
1648
|
+
var SLUG_RE, TASK_CONTEXT_MAX_DOCS, TASK_CONTEXT_MAX_DOC_CHARS, TASK_CONTEXT_MAX_COMMENTS, TASK_CONTEXT_MAX_COMMENT_CHARS, TASKLESS_CONNECT_TIMEOUT_MS;
|
|
1528
1649
|
var init_taskless = __esm({
|
|
1529
1650
|
"src/taskless.ts"() {
|
|
1530
1651
|
"use strict";
|
|
@@ -1533,6 +1654,11 @@ var init_taskless = __esm({
|
|
|
1533
1654
|
init_oauth();
|
|
1534
1655
|
init_workspace();
|
|
1535
1656
|
SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
1657
|
+
TASK_CONTEXT_MAX_DOCS = 5;
|
|
1658
|
+
TASK_CONTEXT_MAX_DOC_CHARS = 4e3;
|
|
1659
|
+
TASK_CONTEXT_MAX_COMMENTS = 30;
|
|
1660
|
+
TASK_CONTEXT_MAX_COMMENT_CHARS = 2e3;
|
|
1661
|
+
TASKLESS_CONNECT_TIMEOUT_MS = 1e4;
|
|
1536
1662
|
}
|
|
1537
1663
|
});
|
|
1538
1664
|
|
|
@@ -1541,7 +1667,7 @@ init_db();
|
|
|
1541
1667
|
import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
|
|
1542
1668
|
import { basename as basename3, dirname as dirname2, join as join7 } from "path";
|
|
1543
1669
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
1544
|
-
import { execFileSync as
|
|
1670
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1545
1671
|
|
|
1546
1672
|
// src/swarmClient.ts
|
|
1547
1673
|
init_config();
|
|
@@ -1618,13 +1744,26 @@ function fileDescriptorLoadRatio() {
|
|
|
1618
1744
|
return null;
|
|
1619
1745
|
}
|
|
1620
1746
|
var RESOURCES_CACHE_MS = 250;
|
|
1621
|
-
var
|
|
1622
|
-
function
|
|
1747
|
+
var readingCache = null;
|
|
1748
|
+
function hostReading() {
|
|
1623
1749
|
const now = Date.now();
|
|
1624
|
-
if (
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1750
|
+
if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
|
|
1751
|
+
const reading = {
|
|
1752
|
+
load1: os.loadavg()[0] ?? 0,
|
|
1753
|
+
cpus: os.cpus().length,
|
|
1754
|
+
freeGb: availableMemBytes() / 1e9,
|
|
1755
|
+
fdLoadRatio: fileDescriptorLoadRatio()
|
|
1756
|
+
};
|
|
1757
|
+
readingCache = { at: now, reading };
|
|
1758
|
+
return reading;
|
|
1759
|
+
}
|
|
1760
|
+
function hostResourcesOk() {
|
|
1761
|
+
const r = hostReading();
|
|
1762
|
+
return resourcesOk(r.load1, r.cpus, r.freeGb, void 0, void 0, r.fdLoadRatio);
|
|
1763
|
+
}
|
|
1764
|
+
function hostReliefNeeded() {
|
|
1765
|
+
const r = hostReading();
|
|
1766
|
+
return !resourcesOk(r.load1, r.cpus, r.freeGb, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, r.fdLoadRatio, SWARM_RELIEF_MAX_FD_LOAD_FACTOR);
|
|
1628
1767
|
}
|
|
1629
1768
|
|
|
1630
1769
|
// src/swarmClient.ts
|
|
@@ -1654,6 +1793,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
1654
1793
|
defaultRepo;
|
|
1655
1794
|
autoFlush;
|
|
1656
1795
|
listLimit;
|
|
1796
|
+
// TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
|
|
1797
|
+
// карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
|
|
1798
|
+
repoFullNames;
|
|
1799
|
+
repoPathByFullName;
|
|
1657
1800
|
// Зеркало.
|
|
1658
1801
|
runs = /* @__PURE__ */ new Map();
|
|
1659
1802
|
runtime = /* @__PURE__ */ new Map();
|
|
@@ -1686,6 +1829,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
1686
1829
|
this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
|
|
1687
1830
|
this.autoFlush = opts.autoFlush ?? true;
|
|
1688
1831
|
this.listLimit = opts.listLimit ?? 200;
|
|
1832
|
+
const registry = opts.repoRegistry ?? [];
|
|
1833
|
+
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
1834
|
+
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
1689
1835
|
this.outboxPath = opts.outboxPath;
|
|
1690
1836
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
1691
1837
|
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
@@ -1932,6 +2078,19 @@ var SwarmClient = class _SwarmClient {
|
|
|
1932
2078
|
(r) => r.reviews_run_id === reviewRunId && r.status !== "failed" && r.status !== "stalled"
|
|
1933
2079
|
).length;
|
|
1934
2080
|
}
|
|
2081
|
+
// Жёсткий кап ревью-спавнов (TASWM-204) — см. db.ts:reviewerAttemptCountFor.
|
|
2082
|
+
reviewerAttemptCountFor(reviewRunId) {
|
|
2083
|
+
return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).length;
|
|
2084
|
+
}
|
|
2085
|
+
// Последняя попытка ревьюера (TASWM-196) — см. db.ts:lastReviewerAttemptFor.
|
|
2086
|
+
lastReviewerAttemptFor(reviewRunId) {
|
|
2087
|
+
const cands = [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).sort((a, b) => b.id - a.id);
|
|
2088
|
+
return cands.length ? this.mat(cands[0]) : null;
|
|
2089
|
+
}
|
|
2090
|
+
// Живые/ждущие ревьюер-прогоны (TASWM-204) — см. db.ts:activeReviewersFor.
|
|
2091
|
+
activeReviewersFor(reviewRunId) {
|
|
2092
|
+
return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId && !TERMINAL_STATUSES.includes(r.status)).sort((a, b) => b.id - a.id).map((d) => this.mat(d));
|
|
2093
|
+
}
|
|
1935
2094
|
activeRunForSlug(slug) {
|
|
1936
2095
|
const cands = [...this.runs.values()].filter((r) => r.task_slug === slug && !SLUG_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
|
|
1937
2096
|
return cands.length ? this.mat(cands[0]) : null;
|
|
@@ -2252,11 +2411,17 @@ var SwarmClient = class _SwarmClient {
|
|
|
2252
2411
|
* cuid известен сразу, никакого create-flush. Возвращает локальный Run (queued; overmind
|
|
2253
2412
|
* заспавнит его следующим тиком) либо null: нет claimable-задачи / прод недоступен / тул
|
|
2254
2413
|
* отверг — во всех случаях тик продолжается, следующий ретраит.
|
|
2414
|
+
*
|
|
2415
|
+
* TASWM-169: repoFullNames — ЖИВОЙ реестр демона на момент ЭТОГО вызова, а не
|
|
2416
|
+
* последняя heartbeat-декларация (heartbeat — своя кадансность, ~15с; claim — ~60с;
|
|
2417
|
+
* плюс окно до первого успешного heartbeat при рестарте) — свежий аргумент здесь
|
|
2418
|
+
* убирает то окно рассинхрона целиком. Пусто (демон без SWARM_REPOS) → ключ не
|
|
2419
|
+
* шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
|
|
2255
2420
|
*/
|
|
2256
2421
|
async claim() {
|
|
2257
2422
|
let res;
|
|
2258
2423
|
try {
|
|
2259
|
-
res = await this.call("swarm_claim_work", {});
|
|
2424
|
+
res = await this.call("swarm_claim_work", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
|
|
2260
2425
|
} catch {
|
|
2261
2426
|
return null;
|
|
2262
2427
|
}
|
|
@@ -2286,16 +2451,41 @@ var SwarmClient = class _SwarmClient {
|
|
|
2286
2451
|
const n = obj?.maxDrones;
|
|
2287
2452
|
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
|
|
2288
2453
|
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
|
|
2456
|
+
* repoFullName (owner/repo из origin — см. git.remoteRepoFullName) — Project.swarmConfig.
|
|
2457
|
+
* verifyMode ("local"/"ci"). Прод недоступен / repo не смэплен на swarm-enabled проект
|
|
2458
|
+
* владельца токена (config=null в ответе) → null: ридер (makeVerifyModeReader) берёт
|
|
2459
|
+
* env-фолбэк SWARM_VERIFY.
|
|
2460
|
+
*/
|
|
2461
|
+
async repoVerifyMode(repoFullName) {
|
|
2462
|
+
let res;
|
|
2463
|
+
try {
|
|
2464
|
+
res = await this.call("swarm_repo_config", { repoFullName });
|
|
2465
|
+
} catch {
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
if (res?.isError) return null;
|
|
2469
|
+
const sc = res.structuredContent ?? {};
|
|
2470
|
+
const v = sc.config?.verifyMode;
|
|
2471
|
+
return v === "local" || v === "ci" ? v : null;
|
|
2472
|
+
}
|
|
2289
2473
|
/**
|
|
2290
2474
|
* Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
|
|
2291
2475
|
* дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
|
|
2292
2476
|
* овермайндом» вместо устаревшего «живого» роя). Best-effort, как ownerMaxDrones/claim:
|
|
2293
2477
|
* прод недоступен/тул отверг → просто не обновили в этом тике, следующий ретраит —
|
|
2294
2478
|
* не роняем тик демона из-за упавшего пульса.
|
|
2479
|
+
*
|
|
2480
|
+
* TASWM-169: заодно декларирует repoFullNames (репо-реестр демона) — та же
|
|
2481
|
+
* кадансность, что и пульс, естественная точка «сказать серверу, что обслуживаю».
|
|
2482
|
+
* Пустой реестр (демон не сконфигурирован на multi-repo/SWARM_REPOS) → ключ не
|
|
2483
|
+
* шлём вовсе (undefined) — прод НЕ трогает прошлую декларацию (см. swarm.ts), не
|
|
2484
|
+
* стирает её ложным «репо больше нет» из-за демона без SWARM_REPOS в env.
|
|
2295
2485
|
*/
|
|
2296
2486
|
async heartbeat() {
|
|
2297
2487
|
try {
|
|
2298
|
-
await this.call("swarm_heartbeat", {});
|
|
2488
|
+
await this.call("swarm_heartbeat", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
|
|
2299
2489
|
} catch {
|
|
2300
2490
|
}
|
|
2301
2491
|
}
|
|
@@ -2329,6 +2519,19 @@ var SwarmClient = class _SwarmClient {
|
|
|
2329
2519
|
});
|
|
2330
2520
|
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
2331
2521
|
}
|
|
2522
|
+
/**
|
|
2523
|
+
* TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
|
|
2524
|
+
* через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
|
|
2525
|
+
* сервер не знает локальных fs-путей. Транслируем в путь по СВОЕМУ репо-реестру
|
|
2526
|
+
* (см. repoRegistry.ts), т.к. git-операции (addWorktree/droneWorktreePath) ждут
|
|
2527
|
+
* именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
|
|
2528
|
+
* repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
|
|
2529
|
+
* (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
|
|
2530
|
+
*/
|
|
2531
|
+
resolveRepo(raw) {
|
|
2532
|
+
if (!raw) return "";
|
|
2533
|
+
return this.repoPathByFullName.get(raw) ?? raw;
|
|
2534
|
+
}
|
|
2332
2535
|
hydrateRun(pr) {
|
|
2333
2536
|
const prodId = String(pr.id ?? "");
|
|
2334
2537
|
if (!prodId) return null;
|
|
@@ -2384,7 +2587,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
2384
2587
|
const revProd = pr.reviewsRunId ?? null;
|
|
2385
2588
|
if (revProd) local.reviews_run_id = this.localIdForProdId(revProd) ?? local.reviews_run_id;
|
|
2386
2589
|
local.review_verdict = pr.reviewVerdict ?? local.review_verdict;
|
|
2387
|
-
local.repo = pr.repo ??
|
|
2590
|
+
local.repo = this.resolveRepo(pr.repo ?? null);
|
|
2388
2591
|
local.branch = pr.branch ?? null;
|
|
2389
2592
|
local.status = pr.status ?? local.status;
|
|
2390
2593
|
local.area = pr.area ?? null;
|
|
@@ -2450,11 +2653,66 @@ var SwarmClient = class _SwarmClient {
|
|
|
2450
2653
|
}, this.backoffMs);
|
|
2451
2654
|
if (typeof this.flushTimer.unref === "function") this.flushTimer.unref();
|
|
2452
2655
|
}
|
|
2453
|
-
|
|
2656
|
+
// TASWM-202: create-флаш без прод-id слал taskId=e.taskRef НАПРЯМУЮ — а taskRef это
|
|
2657
|
+
// человеческий слаг (task_slug, напр. «TASWM-171»), не cuid. Резолверы конфликта
|
|
2658
|
+
// рождаются ЛОКАЛЬНО (createRun с uuid-runKey) и попадают в outbox БЕЗ прод-id —
|
|
2659
|
+
// обычные прогоны рождаются в проде (claim) и гидрируются сразу с cuid, эта ветка
|
|
2660
|
+
// их не касается. Прод create-схема ждёт cuid → отбивает слаг → строго
|
|
2661
|
+
// упорядоченный flush стоит на этой голове НАВЕЧНО (ядовитая голова, инцидент
|
|
2662
|
+
// 14.07/18.07, TASWM-201 — общий карантин; здесь — конкретный источник). Резолвим
|
|
2663
|
+
// слаг→cuid через get_task (кэш — taskIdCache) ПЕРЕД тем как слать create.
|
|
2664
|
+
taskIdCache = /* @__PURE__ */ new Map();
|
|
2665
|
+
// task_slug → cuid прода
|
|
2666
|
+
// Create для этого runKey резолвился окончательно неудачно (задача не найдена/
|
|
2667
|
+
// недоступна) — событие/вопросы того же прогона тоже никогда не получат prod-id,
|
|
2668
|
+
// без карантина они бы бросали «нет prod-id» на каждом флаше и СНОВА вставали
|
|
2669
|
+
// ядовитой головой (просто на одну запись ниже по очереди).
|
|
2670
|
+
poisonedRunKeys = /* @__PURE__ */ new Set();
|
|
2671
|
+
// "transient" — прод/сеть недоступны, ответа нет или ошибка НЕ про отсутствие задачи
|
|
2672
|
+
// (offline-стаб CLI, auth-протух, 5xx): НЕ терминально — запись остаётся в outbox и
|
|
2673
|
+
// повторится на следующем флаше (иначе сетевой сбой = потеря прогона). null —
|
|
2674
|
+
// терминально: прод ЯВНО ответил «задачи нет» (task_not_found / not_found в тексте
|
|
2675
|
+
// tool-ошибки) либо успешный ответ без id — только это карантиним.
|
|
2676
|
+
async resolveTaskCuid(taskRef) {
|
|
2677
|
+
const cached = this.taskIdCache.get(taskRef);
|
|
2678
|
+
if (cached) return cached;
|
|
2679
|
+
let res;
|
|
2680
|
+
try {
|
|
2681
|
+
res = await this.call("get_task", { id: taskRef });
|
|
2682
|
+
} catch {
|
|
2683
|
+
return "transient";
|
|
2684
|
+
}
|
|
2685
|
+
if (!res) return "transient";
|
|
2686
|
+
if (res.isError) {
|
|
2687
|
+
const text = (res.content ?? []).map((c) => c.text ?? "").join(" ");
|
|
2688
|
+
return /not_found/i.test(text) ? null : "transient";
|
|
2689
|
+
}
|
|
2690
|
+
const id = res.structuredContent?.task?.id;
|
|
2691
|
+
if (!id) return null;
|
|
2692
|
+
this.taskIdCache.set(taskRef, id);
|
|
2693
|
+
return id;
|
|
2694
|
+
}
|
|
2695
|
+
async resolveArgs(e) {
|
|
2454
2696
|
if (e.t === "run") {
|
|
2455
2697
|
const fields = this.materializeRefs(e.fields);
|
|
2456
2698
|
const prodId2 = this.prodIdByKey.get(e.runKey);
|
|
2457
|
-
|
|
2699
|
+
if (prodId2) return { tool: "swarm_run_upsert", args: { runId: prodId2, ...fields } };
|
|
2700
|
+
const taskId = await this.resolveTaskCuid(e.taskRef);
|
|
2701
|
+
if (taskId === "transient") {
|
|
2702
|
+
throw new Error(`slug\u2192cuid \u0434\u043B\u044F \xAB${e.taskRef}\xBB \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0440\u043E\u0434 \u043C\u043E\u0440\u0433\u043D\u0443\u043B) \u2014 retry`);
|
|
2703
|
+
}
|
|
2704
|
+
if (!taskId) {
|
|
2705
|
+
console.error(
|
|
2706
|
+
`[swarm-client] create-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u043F\u0440\u043E\u0434 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0443 \xAB${e.taskRef}\xBB (runKey=${e.runKey}) \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`
|
|
2707
|
+
);
|
|
2708
|
+
this.poisonedRunKeys.add(e.runKey);
|
|
2709
|
+
return "skip";
|
|
2710
|
+
}
|
|
2711
|
+
return { tool: "swarm_run_upsert", args: { taskId, ...fields } };
|
|
2712
|
+
}
|
|
2713
|
+
if (this.poisonedRunKeys.has(e.runKey)) {
|
|
2714
|
+
console.error(`[swarm-client] ${e.t}-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: create \u043F\u0440\u043E\u0433\u043E\u043D\u0430 runKey=${e.runKey} \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u0435\u0435 \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`);
|
|
2715
|
+
return "skip";
|
|
2458
2716
|
}
|
|
2459
2717
|
const prodId = this.prodIdByKey.get(e.runKey);
|
|
2460
2718
|
if (!prodId) throw new Error(`\u043D\u0435\u0442 prod-id \u0434\u043B\u044F ${e.runKey} \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D \u0432 \u043F\u0440\u043E\u0434\u0435`);
|
|
@@ -2488,10 +2746,14 @@ var SwarmClient = class _SwarmClient {
|
|
|
2488
2746
|
const e = this.pending[0];
|
|
2489
2747
|
let call;
|
|
2490
2748
|
try {
|
|
2491
|
-
call = this.resolveArgs(e);
|
|
2749
|
+
call = await this.resolveArgs(e);
|
|
2492
2750
|
} catch {
|
|
2493
2751
|
break;
|
|
2494
2752
|
}
|
|
2753
|
+
if (call === "skip") {
|
|
2754
|
+
this.pending.shift();
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2495
2757
|
let res;
|
|
2496
2758
|
try {
|
|
2497
2759
|
res = await this.call(call.tool, call.args);
|
|
@@ -2581,6 +2843,65 @@ function overmindAlive(path, now, staleMs) {
|
|
|
2581
2843
|
return now - ts < staleMs;
|
|
2582
2844
|
}
|
|
2583
2845
|
|
|
2846
|
+
// src/repoRegistry.ts
|
|
2847
|
+
init_config();
|
|
2848
|
+
import { $ } from "execa";
|
|
2849
|
+
function parseRepoFullName(remoteUrl) {
|
|
2850
|
+
const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
|
|
2851
|
+
if (!m) return null;
|
|
2852
|
+
return `${m[1]}/${m[2]}`.toLowerCase();
|
|
2853
|
+
}
|
|
2854
|
+
async function resolveRepoFullName(path) {
|
|
2855
|
+
const res = await $({ reject: false })`git -C ${path} remote get-url origin`;
|
|
2856
|
+
if (res.exitCode !== 0) return null;
|
|
2857
|
+
return parseRepoFullName(res.stdout);
|
|
2858
|
+
}
|
|
2859
|
+
async function buildRepoRegistry(paths) {
|
|
2860
|
+
const resolved = await Promise.all(
|
|
2861
|
+
paths.map(async (raw) => {
|
|
2862
|
+
const path = normalizeRepo(raw);
|
|
2863
|
+
if (!path) return null;
|
|
2864
|
+
const repoFullName = await resolveRepoFullName(path);
|
|
2865
|
+
return repoFullName ? { repoFullName, path } : null;
|
|
2866
|
+
})
|
|
2867
|
+
);
|
|
2868
|
+
return resolved.filter((e) => e !== null);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/relief.ts
|
|
2872
|
+
var numEnv2 = (name, def) => {
|
|
2873
|
+
const v = Number(process.env[name]);
|
|
2874
|
+
return Number.isFinite(v) && v > 0 ? v : def;
|
|
2875
|
+
};
|
|
2876
|
+
var DEFAULT_RELIEF_CONFIG = {
|
|
2877
|
+
rampWindowMs: numEnv2("SWARM_RELIEF_RAMP_WINDOW_MS", 2e4),
|
|
2878
|
+
pauseWindowMs: numEnv2("SWARM_RELIEF_PAUSE_WINDOW_MS", 3e4),
|
|
2879
|
+
sustainMs: numEnv2("SWARM_RELIEF_SUSTAIN_MS", 15e3)
|
|
2880
|
+
};
|
|
2881
|
+
function resolveReliefConfig(cfg) {
|
|
2882
|
+
return { ...DEFAULT_RELIEF_CONFIG, ...cfg ?? {} };
|
|
2883
|
+
}
|
|
2884
|
+
var RELIEF_PAUSE_REASON = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM, \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 --continue), \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u043F\u043E\u0441\u043B\u0435 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 (TASWM-151)";
|
|
2885
|
+
function rampDue(lastRampAt, now, cfg) {
|
|
2886
|
+
return lastRampAt == null || now - lastRampAt >= cfg.rampWindowMs;
|
|
2887
|
+
}
|
|
2888
|
+
function pauseDue(reliefSince, lastPauseAt, now, cfg) {
|
|
2889
|
+
if (reliefSince == null || now - reliefSince < cfg.sustainMs) return false;
|
|
2890
|
+
return lastPauseAt == null || now - lastPauseAt >= cfg.pauseWindowMs;
|
|
2891
|
+
}
|
|
2892
|
+
function pickPauseCandidate(runs) {
|
|
2893
|
+
const candidates = runs.filter(
|
|
2894
|
+
(r) => r.kind === "drone" && r.status === "running" && r.pid != null && r.resolves_run_id == null && r.reviews_run_id == null
|
|
2895
|
+
);
|
|
2896
|
+
if (!candidates.length) return null;
|
|
2897
|
+
return candidates.reduce((youngest, r) => r.created_at > youngest.created_at ? r : youngest);
|
|
2898
|
+
}
|
|
2899
|
+
function pickResumeCandidate(pausedForRelief) {
|
|
2900
|
+
const candidates = pausedForRelief.filter((r) => r.kind === "drone");
|
|
2901
|
+
if (!candidates.length) return null;
|
|
2902
|
+
return candidates.reduce((oldest, r) => r.updated_at < oldest.updated_at ? r : oldest);
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2584
2905
|
// src/launcher.ts
|
|
2585
2906
|
import { spawn } from "child_process";
|
|
2586
2907
|
import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readdirSync, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -2671,7 +2992,10 @@ function makeLauncher(cfg) {
|
|
|
2671
2992
|
overmind: {
|
|
2672
2993
|
command: isCompiled ? process.execPath : "bun",
|
|
2673
2994
|
args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
2674
|
-
|
|
2995
|
+
// SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — устойчив к
|
|
2996
|
+
// пересозданию локального стора между спавном и вызовами read_task (droneServer.ts
|
|
2997
|
+
// доверяет ему, а не db.getRun(run.id).task_slug, который может протухнуть/переехать).
|
|
2998
|
+
env: { SWARM_DB: cfg.dbPath, SWARM_TASK_SLUG: run.task_slug }
|
|
2675
2999
|
}
|
|
2676
3000
|
}
|
|
2677
3001
|
};
|
|
@@ -2698,6 +3022,168 @@ function makeLauncher(cfg) {
|
|
|
2698
3022
|
};
|
|
2699
3023
|
}
|
|
2700
3024
|
|
|
3025
|
+
// src/procCleanup.ts
|
|
3026
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3027
|
+
function sleep(ms) {
|
|
3028
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3029
|
+
}
|
|
3030
|
+
function parseLsofCwd(output) {
|
|
3031
|
+
const rows = [];
|
|
3032
|
+
let pid = null;
|
|
3033
|
+
for (const line of output.split("\n")) {
|
|
3034
|
+
const pMatch = /^p(\d+)$/.exec(line);
|
|
3035
|
+
if (pMatch) {
|
|
3036
|
+
pid = Number(pMatch[1]);
|
|
3037
|
+
} else if (line.startsWith("n") && pid != null) {
|
|
3038
|
+
rows.push({ pid, cwd: line.slice(1) });
|
|
3039
|
+
pid = null;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return rows;
|
|
3043
|
+
}
|
|
3044
|
+
function cwdUnder(cwd, dir) {
|
|
3045
|
+
return cwd === dir || cwd.startsWith(`${dir}/`);
|
|
3046
|
+
}
|
|
3047
|
+
function pidsWithCwdUnder(dir) {
|
|
3048
|
+
let out;
|
|
3049
|
+
try {
|
|
3050
|
+
out = execFileSync2("lsof", ["-a", "-d", "cwd", "-F", "pn"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
3051
|
+
} catch (e) {
|
|
3052
|
+
logTransientSpawnError("lsof", e);
|
|
3053
|
+
return [];
|
|
3054
|
+
}
|
|
3055
|
+
return parseLsofCwd(out).filter((r) => cwdUnder(r.cwd, dir));
|
|
3056
|
+
}
|
|
3057
|
+
function pidAlive(pid) {
|
|
3058
|
+
try {
|
|
3059
|
+
process.kill(pid, 0);
|
|
3060
|
+
return true;
|
|
3061
|
+
} catch (e) {
|
|
3062
|
+
return e.code === "EPERM";
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
async function killPidsGraceful(pids, opts = {}) {
|
|
3066
|
+
const graceMs = opts.graceMs ?? 3e3;
|
|
3067
|
+
const pollMs = opts.pollMs ?? 150;
|
|
3068
|
+
const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
3069
|
+
const isAlive = opts.isAlive ?? pidAlive;
|
|
3070
|
+
const sleepFn = opts.sleepFn ?? sleep;
|
|
3071
|
+
const signalled = [];
|
|
3072
|
+
for (const pid of pids) {
|
|
3073
|
+
try {
|
|
3074
|
+
kill(pid, "SIGTERM");
|
|
3075
|
+
signalled.push(pid);
|
|
3076
|
+
} catch {
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
if (!signalled.length) return signalled;
|
|
3080
|
+
const deadline = Date.now() + graceMs;
|
|
3081
|
+
while (Date.now() < deadline && signalled.some((pid) => isAlive(pid))) {
|
|
3082
|
+
await sleepFn(pollMs);
|
|
3083
|
+
}
|
|
3084
|
+
for (const pid of signalled) {
|
|
3085
|
+
try {
|
|
3086
|
+
if (isAlive(pid)) kill(pid, "SIGKILL");
|
|
3087
|
+
} catch {
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
return signalled;
|
|
3091
|
+
}
|
|
3092
|
+
function killProcessGroup(pid, signal) {
|
|
3093
|
+
try {
|
|
3094
|
+
process.kill(-pid, signal);
|
|
3095
|
+
} catch {
|
|
3096
|
+
process.kill(pid, signal);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function composeProjectPrefix(worktreeDirName) {
|
|
3100
|
+
return worktreeDirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3101
|
+
}
|
|
3102
|
+
function containerBelongsToPrefix(name, prefix) {
|
|
3103
|
+
return name === prefix || name.startsWith(`${prefix}-`);
|
|
3104
|
+
}
|
|
3105
|
+
function findOrphanContainers(containerNames, runs) {
|
|
3106
|
+
const orphans = [];
|
|
3107
|
+
for (const name of containerNames) {
|
|
3108
|
+
let best = null;
|
|
3109
|
+
for (const r of runs) {
|
|
3110
|
+
if (!containerBelongsToPrefix(name, r.prefix)) continue;
|
|
3111
|
+
if (!best || r.prefix.length > best.prefix.length) best = r;
|
|
3112
|
+
}
|
|
3113
|
+
if (best && !best.active) orphans.push(name);
|
|
3114
|
+
}
|
|
3115
|
+
return orphans;
|
|
3116
|
+
}
|
|
3117
|
+
function dockerContainerNames() {
|
|
3118
|
+
try {
|
|
3119
|
+
return execFileSync2("docker", ["ps", "-a", "--format", "{{.Names}}"], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3120
|
+
} catch (e) {
|
|
3121
|
+
logTransientSpawnError("docker ps", e);
|
|
3122
|
+
return [];
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
function removeContainers(names) {
|
|
3126
|
+
const removed = [];
|
|
3127
|
+
for (const name of names) {
|
|
3128
|
+
try {
|
|
3129
|
+
execFileSync2("docker", ["rm", "-f", name], { encoding: "utf8" });
|
|
3130
|
+
removed.push(name);
|
|
3131
|
+
} catch (e) {
|
|
3132
|
+
console.warn(`[procCleanup] docker rm -f ${name} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
return removed;
|
|
3136
|
+
}
|
|
3137
|
+
async function cleanupWorktreeProcesses(worktree, opts = {}) {
|
|
3138
|
+
try {
|
|
3139
|
+
const found = pidsWithCwdUnder(worktree);
|
|
3140
|
+
if (found.length) {
|
|
3141
|
+
const killed = await killPidsGraceful(
|
|
3142
|
+
found.map((f) => f.pid),
|
|
3143
|
+
opts
|
|
3144
|
+
);
|
|
3145
|
+
if (killed.length) console.log(`[procCleanup] worktree ${worktree}: \u043F\u043E\u0433\u0430\u0448\u0435\u043D\u043E \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 ${killed.length} (${killed.join(", ")})`);
|
|
3146
|
+
}
|
|
3147
|
+
} catch (e) {
|
|
3148
|
+
console.error(`[procCleanup] \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
const prefix = composeProjectPrefix(worktree.split("/").filter(Boolean).pop() ?? "");
|
|
3152
|
+
if (!prefix) return;
|
|
3153
|
+
const names = dockerContainerNames().filter((n) => containerBelongsToPrefix(n, prefix));
|
|
3154
|
+
if (!names.length) return;
|
|
3155
|
+
const removed = removeContainers(names);
|
|
3156
|
+
if (removed.length) console.log(`[procCleanup] worktree ${worktree}: \u0441\u043D\u0435\u0441\u0435\u043D\u043E docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 ${removed.length} (${removed.join(", ")})`);
|
|
3157
|
+
} catch (e) {
|
|
3158
|
+
console.error(`[procCleanup] \u0441\u043D\u043E\u0441 docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
async function sweepOrphans(params) {
|
|
3162
|
+
let killedPids = [];
|
|
3163
|
+
try {
|
|
3164
|
+
const found = pidsWithCwdUnder(params.worktreesDir);
|
|
3165
|
+
const orphanPids = found.filter((f) => ![...params.activeWorktreePaths].some((p) => cwdUnder(f.cwd, p))).map((f) => f.pid);
|
|
3166
|
+
if (orphanPids.length) {
|
|
3167
|
+
console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanPids.length} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0446\u0435\u0441\u0441(\u043E\u0432) \u043F\u043E\u0434 ${params.worktreesDir} \u2014 \u0433\u0430\u0448\u0443`, orphanPids);
|
|
3168
|
+
killedPids = await killPidsGraceful(orphanPids, params.killOpts);
|
|
3169
|
+
}
|
|
3170
|
+
} catch (e) {
|
|
3171
|
+
console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
3172
|
+
}
|
|
3173
|
+
let removedContainers = [];
|
|
3174
|
+
try {
|
|
3175
|
+
const names = dockerContainerNames();
|
|
3176
|
+
const orphanNames = findOrphanContainers(names, params.containerRunPrefixes);
|
|
3177
|
+
if (orphanNames.length) {
|
|
3178
|
+
console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanNames.length} docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440(\u043E\u0432)-\u0441\u0438\u0440\u043E\u0442 \u2014 \u0433\u0430\u0448\u0443`, orphanNames);
|
|
3179
|
+
removedContainers = removeContainers(orphanNames);
|
|
3180
|
+
}
|
|
3181
|
+
} catch (e) {
|
|
3182
|
+
console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
3183
|
+
}
|
|
3184
|
+
return { killedPids, removedContainers };
|
|
3185
|
+
}
|
|
3186
|
+
|
|
2701
3187
|
// src/cost.ts
|
|
2702
3188
|
import { readFileSync as readFileSync5, readdirSync as readdirSync2, renameSync as renameSync3 } from "fs";
|
|
2703
3189
|
import { join as join3 } from "path";
|
|
@@ -2789,7 +3275,7 @@ function makeCostAccountant(deps) {
|
|
|
2789
3275
|
}
|
|
2790
3276
|
|
|
2791
3277
|
// src/git.ts
|
|
2792
|
-
import { $ } from "execa";
|
|
3278
|
+
import { $ as $2 } from "execa";
|
|
2793
3279
|
var WorktreeBusyError = class extends Error {
|
|
2794
3280
|
constructor(branch, worktreePath) {
|
|
2795
3281
|
super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
|
|
@@ -2801,10 +3287,10 @@ var WorktreeBusyError = class extends Error {
|
|
|
2801
3287
|
worktreePath;
|
|
2802
3288
|
};
|
|
2803
3289
|
async function revParse(repo, ref) {
|
|
2804
|
-
return (await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
|
|
3290
|
+
return (await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
|
|
2805
3291
|
}
|
|
2806
3292
|
async function worktreeForBranch(repo, branch) {
|
|
2807
|
-
const out = (await
|
|
3293
|
+
const out = (await $2`git -C ${repo} worktree list --porcelain`).stdout;
|
|
2808
3294
|
const target = `refs/heads/${branch}`;
|
|
2809
3295
|
let current = null;
|
|
2810
3296
|
for (const line of out.split("\n")) {
|
|
@@ -2813,22 +3299,29 @@ async function worktreeForBranch(repo, branch) {
|
|
|
2813
3299
|
}
|
|
2814
3300
|
return null;
|
|
2815
3301
|
}
|
|
2816
|
-
async function addWorktree(repo, path, branch, base2
|
|
3302
|
+
async function addWorktree(repo, path, branch, base2) {
|
|
2817
3303
|
const localSha = await revParse(repo, `refs/heads/${branch}`);
|
|
2818
3304
|
const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
|
|
2819
3305
|
if (!localSha && !originSha) {
|
|
2820
|
-
|
|
3306
|
+
const freshBase = base2 ?? await freshWorktreeBase(repo);
|
|
3307
|
+
await $2`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
|
|
2821
3308
|
return { reused: false };
|
|
2822
3309
|
}
|
|
2823
3310
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
2824
3311
|
if (busyAt) throw new WorktreeBusyError(branch, busyAt);
|
|
2825
3312
|
if (originSha) {
|
|
2826
|
-
await
|
|
3313
|
+
await $2`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
|
|
2827
3314
|
return { reused: true, from: "origin", sha: originSha };
|
|
2828
3315
|
}
|
|
2829
|
-
await
|
|
3316
|
+
await $2`git -C ${repo} worktree add ${path} ${branch}`;
|
|
2830
3317
|
return { reused: true, from: "local" };
|
|
2831
3318
|
}
|
|
3319
|
+
async function freshWorktreeBase(repo) {
|
|
3320
|
+
const hasOrigin = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).exitCode === 0;
|
|
3321
|
+
if (!hasOrigin) return "HEAD";
|
|
3322
|
+
await $2`git -C ${repo} fetch --quiet origin`;
|
|
3323
|
+
return remoteMainRef(repo);
|
|
3324
|
+
}
|
|
2832
3325
|
function worktreeSourceMessage(branch, res) {
|
|
2833
3326
|
if (!res.reused) return null;
|
|
2834
3327
|
if (res.from === "origin") return `\u0432\u0435\u0442\u043A\u0430 ${branch} \u043F\u0435\u0440\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0430 \u0441 origin/${branch} @ ${res.sha.slice(0, 7)}`;
|
|
@@ -2836,49 +3329,56 @@ function worktreeSourceMessage(branch, res) {
|
|
|
2836
3329
|
}
|
|
2837
3330
|
async function setWorktreeIdentity(worktree, name) {
|
|
2838
3331
|
const local = name.toLowerCase().replace(/\s+/g, "-");
|
|
2839
|
-
await
|
|
2840
|
-
await
|
|
2841
|
-
await
|
|
3332
|
+
await $2`git -C ${worktree} config extensions.worktreeConfig true`;
|
|
3333
|
+
await $2`git -C ${worktree} config --worktree user.name ${`${name} (swarm drone)`}`;
|
|
3334
|
+
await $2`git -C ${worktree} config --worktree user.email ${`${local}@drones.swarm.local`}`;
|
|
2842
3335
|
}
|
|
2843
3336
|
async function removeWorktree(repo, path) {
|
|
2844
|
-
const branch = (await $({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
2845
|
-
await
|
|
2846
|
-
|
|
3337
|
+
const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
3338
|
+
await cleanupWorktreeProcesses(path);
|
|
3339
|
+
await $2({ reject: false })`git -C ${repo} worktree remove --force ${path}`;
|
|
3340
|
+
if (branch) await $2({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
2847
3341
|
}
|
|
2848
3342
|
async function worktreeDirty(path) {
|
|
2849
|
-
const status = (await $({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
|
|
3343
|
+
const status = (await $2({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
|
|
2850
3344
|
if (status) return true;
|
|
2851
|
-
const unpushed = (await $({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
|
|
3345
|
+
const unpushed = (await $2({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
|
|
2852
3346
|
return unpushed !== "" && unpushed !== "0";
|
|
2853
3347
|
}
|
|
2854
3348
|
async function remoteMainRef(repo) {
|
|
2855
|
-
const head = (await $({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
3349
|
+
const head = (await $2({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
2856
3350
|
if (head) return head;
|
|
2857
3351
|
for (const cand of ["origin/main", "origin/master"]) {
|
|
2858
|
-
const ok = await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
|
|
3352
|
+
const ok = await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
|
|
2859
3353
|
if (ok.exitCode === 0) return cand;
|
|
2860
3354
|
}
|
|
2861
3355
|
throw new Error(`\u043D\u0435 \u043D\u0430\u0448\u0451\u043B \u0434\u0435\u0444\u043E\u043B\u0442-\u0432\u0435\u0442\u043A\u0443 origin \u0432 ${repo} (\u043D\u0435\u0442 origin/HEAD, origin/main, origin/master)`);
|
|
2862
3356
|
}
|
|
3357
|
+
var GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
|
|
3358
|
+
async function remoteRepoFullName(repo) {
|
|
3359
|
+
const url = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).stdout.trim();
|
|
3360
|
+
const m = url.match(GITHUB_REMOTE_RE);
|
|
3361
|
+
return m ? m[1] : null;
|
|
3362
|
+
}
|
|
2863
3363
|
async function prepareDetachedWorktree(repo, branch, worktree) {
|
|
2864
|
-
await
|
|
2865
|
-
await
|
|
3364
|
+
await $2`git -C ${repo} fetch --quiet origin`;
|
|
3365
|
+
await $2`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
|
|
2866
3366
|
}
|
|
2867
3367
|
async function mergeRef(worktree, ref) {
|
|
2868
|
-
const before = (await
|
|
2869
|
-
const merge = await $({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
|
|
3368
|
+
const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
|
|
3369
|
+
const merge = await $2({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
|
|
2870
3370
|
if (merge.exitCode === 0) {
|
|
2871
|
-
const after = (await
|
|
3371
|
+
const after = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
|
|
2872
3372
|
return { clean: true, conflicts: [], changed: after !== before };
|
|
2873
3373
|
}
|
|
2874
|
-
const u = (await $({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
|
|
3374
|
+
const u = (await $2({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
|
|
2875
3375
|
return { clean: false, conflicts: u ? u.split("\n") : [], changed: false };
|
|
2876
3376
|
}
|
|
2877
3377
|
async function pushHeadTo(worktree, branch) {
|
|
2878
|
-
await
|
|
3378
|
+
await $2`git -C ${worktree} push origin ${`HEAD:${branch}`}`;
|
|
2879
3379
|
}
|
|
2880
3380
|
async function worktreeLastCommitMs(worktree) {
|
|
2881
|
-
const out = (await $({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
|
|
3381
|
+
const out = (await $2({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
|
|
2882
3382
|
const sec = Number(out);
|
|
2883
3383
|
return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
|
|
2884
3384
|
}
|
|
@@ -2912,9 +3412,10 @@ async function spawnDrone(deps, run) {
|
|
|
2912
3412
|
}
|
|
2913
3413
|
|
|
2914
3414
|
// src/gh.ts
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
var
|
|
3415
|
+
init_src();
|
|
3416
|
+
import { $ as $3 } from "execa";
|
|
3417
|
+
var realRunner = async (args) => (await $3`gh ${args}`).stdout;
|
|
3418
|
+
var realRepoRunner = async (args, cwd) => (await $3({ cwd })`gh ${args}`).stdout;
|
|
2918
3419
|
var GH_PR_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+(?:[/?#].*)?$/i;
|
|
2919
3420
|
function isGithubPrUrl(url) {
|
|
2920
3421
|
return GH_PR_RE.test(url.trim());
|
|
@@ -2960,10 +3461,53 @@ async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
|
2960
3461
|
return [];
|
|
2961
3462
|
}
|
|
2962
3463
|
}
|
|
3464
|
+
async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
|
|
3465
|
+
if (!branch || !repoDir) return null;
|
|
3466
|
+
try {
|
|
3467
|
+
const out = await run(["pr", "list", "--head", branch, "--state", "merged", "--json", "url", "--limit", "1"], repoDir);
|
|
3468
|
+
const arr = JSON.parse(out);
|
|
3469
|
+
const url = Array.isArray(arr) ? arr[0]?.url : void 0;
|
|
3470
|
+
return typeof url === "string" ? url : null;
|
|
3471
|
+
} catch {
|
|
3472
|
+
return null;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
2963
3475
|
async function closePR(prUrl, run = realRunner) {
|
|
2964
3476
|
await run(["pr", "close", prUrl, "--comment", "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 (swarm)"]);
|
|
2965
3477
|
}
|
|
2966
|
-
var
|
|
3478
|
+
var realCommentsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json comments`).stdout;
|
|
3479
|
+
var VERDICT_RE = /^\s*\**\s*(APPROVE|REQUEST\s+CHANGES)\b/i;
|
|
3480
|
+
function verdictLabel(verdict) {
|
|
3481
|
+
return verdict === "approve" ? "\u2705 APPROVE" : "\u274C REQUEST CHANGES";
|
|
3482
|
+
}
|
|
3483
|
+
async function latestVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
|
|
3484
|
+
if (!isGithubPrUrl(prUrl)) return null;
|
|
3485
|
+
let out;
|
|
3486
|
+
try {
|
|
3487
|
+
out = await run(prUrl);
|
|
3488
|
+
} catch {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
let j;
|
|
3492
|
+
try {
|
|
3493
|
+
j = JSON.parse(out);
|
|
3494
|
+
} catch {
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
const comments = Array.isArray(j.comments) ? j.comments : [];
|
|
3498
|
+
const sinceMs = sqliteMs(sinceIso);
|
|
3499
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
3500
|
+
const c = comments[i];
|
|
3501
|
+
if (!c?.body || !c.createdAt) continue;
|
|
3502
|
+
if (Number.isFinite(sinceMs) && Date.parse(c.createdAt) < sinceMs) continue;
|
|
3503
|
+
const m = c.body.match(VERDICT_RE);
|
|
3504
|
+
if (!m) continue;
|
|
3505
|
+
const verdict = /^APPROVE/i.test(m[1]) ? "approve" : "changes";
|
|
3506
|
+
return { verdict, createdAt: c.createdAt };
|
|
3507
|
+
}
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
var realChecksRunner = async (pr) => (await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`).stdout;
|
|
2967
3511
|
async function prCheckRows(pr, run) {
|
|
2968
3512
|
try {
|
|
2969
3513
|
const out = await run(pr);
|
|
@@ -3016,7 +3560,7 @@ async function prCiStatus(pr, run = realChecksRunner) {
|
|
|
3016
3560
|
if (rows.some((r) => r.bucket === "pending")) return { bucket: "pending", failing: [], failingRunIds: [] };
|
|
3017
3561
|
return { bucket: "pass", failing: [], failingRunIds: [] };
|
|
3018
3562
|
}
|
|
3019
|
-
var realRunLogRunner = async (runId) => (await $
|
|
3563
|
+
var realRunLogRunner = async (runId) => (await $3({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`).stdout;
|
|
3020
3564
|
var CI_SUMMARY_LOG_CHARS = 4e3;
|
|
3021
3565
|
var CI_SUMMARY_MAX_RUNS = 2;
|
|
3022
3566
|
async function ciFailureSummary(failing, failingRunIds, logRun = realRunLogRunner) {
|
|
@@ -3037,9 +3581,11 @@ ${logs.join("\n---\n")}` : header;
|
|
|
3037
3581
|
}
|
|
3038
3582
|
|
|
3039
3583
|
// src/droneServer.ts
|
|
3584
|
+
init_taskless();
|
|
3040
3585
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3041
3586
|
import { z } from "zod";
|
|
3042
3587
|
init_config();
|
|
3588
|
+
init_src();
|
|
3043
3589
|
function freezeInstruction(slug) {
|
|
3044
3590
|
return [
|
|
3045
3591
|
"\u26D4 OVERMIND \u041D\u0415\u0414\u041E\u0421\u0422\u0423\u041F\u0415\u041D \u2014 \u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B. \u041D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0430\u0434 \u0442\u043E\u0431\u043E\u0439 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442: \u043D\u0435\u043A\u043E\u043C\u0443",
|
|
@@ -3080,7 +3626,7 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
3080
3626
|
`overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0443\u043B\u044C\u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u043B > ${Math.round(heartbeatStaleMs / 1e3)}\u0441) \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430`
|
|
3081
3627
|
);
|
|
3082
3628
|
}
|
|
3083
|
-
const slug = run?.task_slug ?? "<slug>";
|
|
3629
|
+
const slug = opts.expectedTaskSlug ?? run?.task_slug ?? "<slug>";
|
|
3084
3630
|
return { content: [{ type: "text", text: freezeInstruction(slug) }] };
|
|
3085
3631
|
};
|
|
3086
3632
|
server.tool(
|
|
@@ -3093,6 +3639,47 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
3093
3639
|
return { content: [{ type: "text", text: "ok" }] };
|
|
3094
3640
|
}
|
|
3095
3641
|
);
|
|
3642
|
+
server.tool(
|
|
3643
|
+
"read_task",
|
|
3644
|
+
"\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C). \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
|
|
3645
|
+
{},
|
|
3646
|
+
async () => {
|
|
3647
|
+
if (!opts.taskless) {
|
|
3648
|
+
return {
|
|
3649
|
+
content: [{ type: "text", text: "Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0441\u043D\u0430\u043F\u0448\u043E\u0442\u0430 \u043D\u0435\u0442, \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435." }],
|
|
3650
|
+
isError: true
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
const run = db.getRun(runId);
|
|
3654
|
+
const slug = opts.expectedTaskSlug ?? run?.task_slug;
|
|
3655
|
+
let mismatchNote;
|
|
3656
|
+
if (opts.expectedTaskSlug && !run) {
|
|
3657
|
+
mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0441\u0442\u043E\u0440\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
|
|
3658
|
+
} else if (opts.expectedTaskSlug && run && run.task_slug !== opts.expectedTaskSlug) {
|
|
3659
|
+
mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0441\u0435\u0439\u0447\u0430\u0441 \u0447\u0438\u0441\u043B\u0438\u0442\u0441\u044F \u043A\u0430\u043A \xAB${run.task_slug}\xBB \u2014 \u044D\u0442\u043E \u041D\u0415 \u0442\u0432\u043E\u044F \u0437\u0430\u0434\u0430\u0447\u0430 (\u0441\u0442\u043E\u0440 \u0434\u0435\u043C\u043E\u043D\u0430 \u0440\u0430\u0441\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D, TASWM-211). \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0434\u043E\u0432\u0435\u0440\u0435\u043D\u043D\u044B\u0439 slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
|
|
3660
|
+
}
|
|
3661
|
+
if (!slug) return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D" }], isError: true };
|
|
3662
|
+
const task = await opts.taskless.getTask(slug);
|
|
3663
|
+
if (!task) {
|
|
3664
|
+
const notFoundText = `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 ${slug} \u0438\u0437 Taskless (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435.`;
|
|
3665
|
+
return {
|
|
3666
|
+
content: [{ type: "text", text: mismatchNote ? `${mismatchNote}
|
|
3667
|
+
|
|
3668
|
+
${notFoundText}` : notFoundText }],
|
|
3669
|
+
isError: true
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
const context = await fetchTaskContext(opts.taskless, task);
|
|
3673
|
+
const lines = [
|
|
3674
|
+
`**${task.title}** \xB7 ${task.slug}${task.url ? ` (${task.url})` : ""}`,
|
|
3675
|
+
"",
|
|
3676
|
+
task.description || "(\u043F\u0443\u0441\u0442\u043E)"
|
|
3677
|
+
];
|
|
3678
|
+
if (context) lines.push("", context);
|
|
3679
|
+
if (mismatchNote) lines.unshift(mismatchNote, "");
|
|
3680
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
3681
|
+
}
|
|
3682
|
+
);
|
|
3096
3683
|
server.tool(
|
|
3097
3684
|
"ask",
|
|
3098
3685
|
"\u0417\u0430\u0434\u0430\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u043E\u0442\u0432\u0435\u0442\u0430. \u0415\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F \u2014 \u0434\u0430\u0439 options (2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430, \u0441\u0442\u0430\u043D\u0443\u0442 \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438); \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0432\u044B\u0431\u0435\u0440\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0438\u043B\u0438 \u043D\u0430\u043F\u0438\u0448\u0435\u0442 \u0441\u0432\u043E\u0451. \u041E\u043F\u0443\u0441\u0442\u0438 options \u0434\u043B\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442 \u0441\u0442\u0440\u043E\u043A\u043E\u0439. \u0417\u043E\u0432\u0438, \u043A\u043E\u0433\u0434\u0430 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u2014 \u043D\u0435 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439.",
|
|
@@ -3136,7 +3723,7 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
3136
3723
|
{
|
|
3137
3724
|
pr_url: z.string().describe("\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0439 GitHub-PR; \u0434\u043B\u044F \u0437\u0430\u0434\u0430\u0447 \u0431\u0435\u0437 PR \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044F)"),
|
|
3138
3725
|
verdict: z.enum(["approve", "changes"]).optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR \u2014 approve (APPROVE) \u0438\u043B\u0438 changes (REQUEST CHANGES). \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430."),
|
|
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
|
|
3726
|
+
summary: z.string().optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B/\u043D\u0430\u0448\u0451\u043B \u2014 \u0443\u0439\u0434\u0451\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A.")
|
|
3140
3727
|
},
|
|
3141
3728
|
async ({ pr_url, verdict, summary }) => {
|
|
3142
3729
|
const run = db.getRun(runId);
|
|
@@ -3147,6 +3734,9 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
3147
3734
|
};
|
|
3148
3735
|
}
|
|
3149
3736
|
if (run?.reviews_run_id != null) {
|
|
3737
|
+
if (isTerminalStatus(run.status)) {
|
|
3738
|
+
return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442 (PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D" }] };
|
|
3739
|
+
}
|
|
3150
3740
|
if (verdict !== "approve" && verdict !== "changes") {
|
|
3151
3741
|
return {
|
|
3152
3742
|
content: [{ type: "text", text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u2014 done() \u0442\u0440\u0435\u0431\u0443\u0435\u0442 verdict: "approve" | "changes" (\u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR).' }],
|
|
@@ -3154,21 +3744,15 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
3154
3744
|
};
|
|
3155
3745
|
}
|
|
3156
3746
|
const reviewed = db.getRun(run.reviews_run_id);
|
|
3157
|
-
const verdictText = verdict
|
|
3747
|
+
const verdictText = verdictLabel(verdict);
|
|
3158
3748
|
db.updateRun(run.reviews_run_id, { review_verdict: verdict });
|
|
3159
3749
|
db.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043E\u0442 ${run.name}: ${verdictText}${summary ? ` \u2014 ${summary}` : ""}`);
|
|
3160
|
-
if (opts.taskless) {
|
|
3750
|
+
if (opts.taskless && reviewed) {
|
|
3161
3751
|
const note = `${verdictText} \u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${run.name} (\u043F\u0440\u043E\u0433\u043E\u043D #${runId})${summary ? `
|
|
3162
3752
|
|
|
3163
3753
|
${summary}` : ""}`;
|
|
3164
|
-
await
|
|
3165
|
-
|
|
3166
|
-
}),
|
|
3167
|
-
// REV-задача
|
|
3168
|
-
reviewed ? opts.taskless.addComment(reviewed.task_slug, note).catch(() => {
|
|
3169
|
-
}) : Promise.resolve()
|
|
3170
|
-
// задача-источник
|
|
3171
|
-
]);
|
|
3754
|
+
await opts.taskless.addComment(reviewed.task_slug, note).catch(() => {
|
|
3755
|
+
});
|
|
3172
3756
|
}
|
|
3173
3757
|
const trustedPrUrl = reviewed?.pr_url ?? pr_url;
|
|
3174
3758
|
db.updateRun(runId, { status: "review", pr_url: trustedPrUrl });
|
|
@@ -3207,6 +3791,7 @@ ${summary}` : ""}`;
|
|
|
3207
3791
|
if (!taskless) return errText("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C.");
|
|
3208
3792
|
const ep = await epic();
|
|
3209
3793
|
if (!ep) return errText(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0438\u0437 Taskless \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430.`);
|
|
3794
|
+
if (!ep.projectId) return errText(`\u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (\u043D\u0443\u0436\u0435\u043D projectId).`);
|
|
3210
3795
|
const statusId = await taskless.backlogStatusId();
|
|
3211
3796
|
if (!statusId) return errText("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0441\u0442\u0430\u0442\u0443\u0441 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 backlog \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430 \u2014 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u044E (\u0440\u0438\u0441\u043A \u043F\u043E\u043F\u0430\u0441\u0442\u044C \u0441\u0440\u0430\u0437\u0443 \u0432 \xAB\u0421\u0434\u0435\u043B\u0430\u0442\u044C\xBB).");
|
|
3212
3797
|
const footer = `
|
|
@@ -3309,9 +3894,9 @@ async function runDroneServer(runId) {
|
|
|
3309
3894
|
const { DB_PATH: DB_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3310
3895
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3311
3896
|
const db = new SwarmDb2(DB_PATH2);
|
|
3312
|
-
const
|
|
3313
|
-
const
|
|
3314
|
-
const server = buildDroneServer(db, runId, { taskless });
|
|
3897
|
+
const taskless = await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless();
|
|
3898
|
+
const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
|
|
3899
|
+
const server = buildDroneServer(db, runId, { taskless, ...expectedTaskSlug ? { expectedTaskSlug } : {} });
|
|
3315
3900
|
await server.connect(new StdioServerTransport());
|
|
3316
3901
|
}
|
|
3317
3902
|
|
|
@@ -3337,6 +3922,24 @@ function makeMaxDronesReader(opts) {
|
|
|
3337
3922
|
};
|
|
3338
3923
|
}
|
|
3339
3924
|
|
|
3925
|
+
// src/verifyMode.ts
|
|
3926
|
+
function makeVerifyModeReader(opts) {
|
|
3927
|
+
const ttl = opts.ttlMs ?? 6e4;
|
|
3928
|
+
const now = opts.now ?? Date.now;
|
|
3929
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3930
|
+
return async (repo) => {
|
|
3931
|
+
const t = now();
|
|
3932
|
+
const hit = cache.get(repo);
|
|
3933
|
+
if (hit && t - hit.at < ttl) return hit.mode;
|
|
3934
|
+
const live = await opts.fetch(repo).catch(() => null);
|
|
3935
|
+
if (live === "local" || live === "ci") {
|
|
3936
|
+
cache.set(repo, { mode: live, at: t });
|
|
3937
|
+
return live;
|
|
3938
|
+
}
|
|
3939
|
+
return opts.fallback;
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3340
3943
|
// src/web.ts
|
|
3341
3944
|
init_src();
|
|
3342
3945
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -3346,7 +3949,7 @@ import { serve } from "@hono/node-server";
|
|
|
3346
3949
|
// src/diff.ts
|
|
3347
3950
|
import { statSync as statSync2 } from "fs";
|
|
3348
3951
|
import { join as join6 } from "path";
|
|
3349
|
-
import { $ as $
|
|
3952
|
+
import { $ as $4 } from "execa";
|
|
3350
3953
|
var MAX_FILES = 200;
|
|
3351
3954
|
var MAX_LINES_PER_FILE = 1500;
|
|
3352
3955
|
var MAX_UNTRACKED = 60;
|
|
@@ -3466,12 +4069,12 @@ function truncateFile(f) {
|
|
|
3466
4069
|
}
|
|
3467
4070
|
async function resolveBase(wt) {
|
|
3468
4071
|
const tryRef = async (ref) => {
|
|
3469
|
-
const sha = (await $
|
|
4072
|
+
const sha = (await $4({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
|
|
3470
4073
|
if (!sha) return null;
|
|
3471
|
-
const mb = (await $
|
|
4074
|
+
const mb = (await $4({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
|
|
3472
4075
|
return mb || null;
|
|
3473
4076
|
};
|
|
3474
|
-
const originHead = (await $
|
|
4077
|
+
const originHead = (await $4({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
3475
4078
|
const candidates = [originHead, "origin/main", "origin/master", "main", "master"].filter(Boolean);
|
|
3476
4079
|
for (const c of candidates) {
|
|
3477
4080
|
const mb = await tryRef(c);
|
|
@@ -3490,16 +4093,16 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
3490
4093
|
const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
|
|
3491
4094
|
const gone = opts.status === "queued" ? "\u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442" : "worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043A\u043E\u0434 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 PR";
|
|
3492
4095
|
if (!worktree) return stub(gone);
|
|
3493
|
-
const isWt = (await $
|
|
4096
|
+
const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
|
|
3494
4097
|
if (isWt !== "true") return stub(gone);
|
|
3495
4098
|
const base2 = await resolveBase(worktree);
|
|
3496
4099
|
const from = base2 ?? "HEAD";
|
|
3497
|
-
const trackedTxt = (await $
|
|
4100
|
+
const trackedTxt = (await $4({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
|
|
3498
4101
|
let files = parseUnifiedDiff(trackedTxt);
|
|
3499
|
-
const untrackedRaw = (await $
|
|
4102
|
+
const untrackedRaw = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
|
|
3500
4103
|
const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
|
|
3501
4104
|
for (const rel of untracked) {
|
|
3502
|
-
const t = (await $
|
|
4105
|
+
const t = (await $4({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
|
|
3503
4106
|
for (const pf of parseUnifiedDiff(t)) {
|
|
3504
4107
|
pf.status = "added";
|
|
3505
4108
|
if (!pf.path || pf.path === "/dev/null") pf.path = rel;
|
|
@@ -3692,7 +4295,8 @@ function timelineState(db, now = Date.now(), overmindLog = []) {
|
|
|
3692
4295
|
cost: r.cost_usd || 0,
|
|
3693
4296
|
role: r.kind,
|
|
3694
4297
|
heroRunId: r.hero_run_id,
|
|
3695
|
-
reviewVerdict: r.review_verdict
|
|
4298
|
+
reviewVerdict: r.review_verdict,
|
|
4299
|
+
model: r.model
|
|
3696
4300
|
});
|
|
3697
4301
|
}
|
|
3698
4302
|
const liveLanes = lanes.filter((l) => l.live);
|
|
@@ -4190,7 +4794,7 @@ function cardStep(d){
|
|
|
4190
4794
|
if(d.status==='stalled') return d.reason || '\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431';
|
|
4191
4795
|
if(d.status==='stopped') return d.stopReason || d.reason || '\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D';
|
|
4192
4796
|
if(d.status==='storm_dead') return '\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 (Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442), worktree \u0446\u0435\u043B, \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043E\u0442\u0431\u043E\u044F';
|
|
4193
|
-
if(d.status==='paused') return '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430';
|
|
4797
|
+
if(d.status==='paused') return d.stopReason || '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430'; // TASWM-151: \u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0437\u0430\u0434\u0430\u0451\u0442 \u0441\u0432\u043E\u0439 stopReason
|
|
4194
4798
|
if(d.status==='review') { var n=prNum(d.pr); return 'PR'+(n?(' #'+n):'')+' \u043D\u0430 \u0440\u0435\u0432\u044C\u044E'; }
|
|
4195
4799
|
if(d.status==='done') return '\u0441\u043C\u0435\u0440\u0436\u0435\u043D';
|
|
4196
4800
|
if(d.status==='queued') return (d.waiting&&d.waiting.length) ? '\u0436\u0434\u0451\u0442: '+d.waiting.join(', ') : '\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430';
|
|
@@ -5014,21 +5618,15 @@ var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
|
|
|
5014
5618
|
var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
|
|
5015
5619
|
var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
|
|
5016
5620
|
var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
|
|
5621
|
+
var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
|
|
5622
|
+
var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
|
|
5017
5623
|
var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
|
|
5018
5624
|
var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
|
|
5019
5625
|
var CI_CHECK_COOLDOWN_MS = Number(process.env.SWARM_CI_CHECK_COOLDOWN_MS) || 60 * 1e3;
|
|
5020
5626
|
var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u0441\u044C";
|
|
5021
|
-
function pidAlive(pid) {
|
|
5022
|
-
try {
|
|
5023
|
-
process.kill(pid, 0);
|
|
5024
|
-
return true;
|
|
5025
|
-
} catch (e) {
|
|
5026
|
-
return e.code === "EPERM";
|
|
5027
|
-
}
|
|
5028
|
-
}
|
|
5029
5627
|
function psLines(extraArgs) {
|
|
5030
5628
|
try {
|
|
5031
|
-
return
|
|
5629
|
+
return execFileSync3("ps", ["-A", "-ww", ...extraArgs], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split(
|
|
5032
5630
|
"\n"
|
|
5033
5631
|
);
|
|
5034
5632
|
} catch (e) {
|
|
@@ -5114,6 +5712,91 @@ function countActiveDrones(db) {
|
|
|
5114
5712
|
function countActiveReviewers(db) {
|
|
5115
5713
|
return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
5116
5714
|
}
|
|
5715
|
+
async function closeReviewersOnMerge(deps, reviewRunId) {
|
|
5716
|
+
const stale = deps.db.activeReviewersFor(reviewRunId);
|
|
5717
|
+
for (const rv of stale) {
|
|
5718
|
+
if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
|
|
5719
|
+
try {
|
|
5720
|
+
deps.kill(rv.pid);
|
|
5721
|
+
} catch {
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
|
|
5725
|
+
});
|
|
5726
|
+
deps.db.updateRun(rv.id, { status: "done" });
|
|
5727
|
+
deps.db.addEvent(rv.id, "done", "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
async function finalizeMergedReview(deps, run) {
|
|
5731
|
+
await deps.finishTask(run.task_slug);
|
|
5732
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5733
|
+
});
|
|
5734
|
+
deps.db.addEvent(run.id, "merged", run.pr_url ?? "");
|
|
5735
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
5736
|
+
deps.db.addEvent(run.id, "done");
|
|
5737
|
+
await closeReviewersOnMerge(deps, run.id);
|
|
5738
|
+
}
|
|
5739
|
+
function doneMergedPrForSlug(db, slug) {
|
|
5740
|
+
const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
|
|
5741
|
+
return done?.pr_url ?? null;
|
|
5742
|
+
}
|
|
5743
|
+
async function closeDuplicateTaskRun(deps, run, mergedUrl) {
|
|
5744
|
+
if (run.pid != null && deps.kill && deps.isAlive(run.pid)) {
|
|
5745
|
+
try {
|
|
5746
|
+
deps.kill(run.pid);
|
|
5747
|
+
} catch {
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5751
|
+
});
|
|
5752
|
+
const reason = `PR \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D (${mergedUrl}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D-\u0434\u0443\u0431\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430/\u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0438 (TASWM-206)`;
|
|
5753
|
+
deps.db.updateRun(run.id, { status: "superseded", stop_reason: reason });
|
|
5754
|
+
deps.db.addEvent(run.id, "superseded", reason);
|
|
5755
|
+
}
|
|
5756
|
+
async function closeMergedTaskRuns(deps) {
|
|
5757
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
5758
|
+
for (const run of deps.db.listRuns()) {
|
|
5759
|
+
if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
5760
|
+
let group = bySlug.get(run.task_slug);
|
|
5761
|
+
if (!group) bySlug.set(run.task_slug, group = []);
|
|
5762
|
+
group.push(run);
|
|
5763
|
+
}
|
|
5764
|
+
for (const [slug, group] of bySlug) {
|
|
5765
|
+
const nonTerminal = group.filter((r) => !isTerminalStatus(r.status));
|
|
5766
|
+
if (nonTerminal.length === 0) continue;
|
|
5767
|
+
if (nonTerminal.length === 1 && nonTerminal[0].status === "review") continue;
|
|
5768
|
+
try {
|
|
5769
|
+
const withPr = [...group].sort((a, b) => b.id - a.id).find((r) => r.pr_url);
|
|
5770
|
+
let mergedUrl = null;
|
|
5771
|
+
if (withPr) {
|
|
5772
|
+
if (withPr.status === "done") {
|
|
5773
|
+
mergedUrl = withPr.pr_url;
|
|
5774
|
+
} else {
|
|
5775
|
+
const st = await deps.prStatus(withPr.pr_url);
|
|
5776
|
+
if (st.merged) mergedUrl = withPr.pr_url;
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
if (!mergedUrl && deps.mergedPrForBranch) {
|
|
5780
|
+
mergedUrl = await deps.mergedPrForBranch(slug, group[0].repo || "").catch(() => null);
|
|
5781
|
+
}
|
|
5782
|
+
if (!mergedUrl) continue;
|
|
5783
|
+
const canonical = withPr && withPr.pr_url === mergedUrl && !isTerminalStatus(withPr.status) ? withPr : null;
|
|
5784
|
+
for (const run of nonTerminal) {
|
|
5785
|
+
if (canonical && run.id === canonical.id) {
|
|
5786
|
+
await finalizeMergedReview(deps, run);
|
|
5787
|
+
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430 (\u0434\u0443\u0431\u043B\u044C-\u0440\u0430\u043D\u044B \u0440\u043E\u044F, TASWM-206).`);
|
|
5788
|
+
} else {
|
|
5789
|
+
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
5790
|
+
narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0434\u0443\u0431\u043B\u044C.`);
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
if (!canonical) await deps.finishTask(slug).catch(() => {
|
|
5794
|
+
});
|
|
5795
|
+
} catch (e) {
|
|
5796
|
+
console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0434\u0443\u0431\u043B\u0435\u0439-\u0440\u0430\u043D\u043E\u0432 \u0437\u0430\u0434\u0430\u0447\u0438 ${slug} (TASWM-206) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5117
5800
|
var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
|
|
5118
5801
|
function buildCiWakeNudge(summary) {
|
|
5119
5802
|
return [
|
|
@@ -5126,7 +5809,7 @@ function buildCiWakeNudge(summary) {
|
|
|
5126
5809
|
}
|
|
5127
5810
|
var HERO_CONTINUE_NUDGE = "\u0422\u0435\u0431\u044F \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B\u0438, \u0413\u0435\u0440\u043E\u0439: \u0432 \u043E\u0442\u0440\u044F\u0434\u0435 \u0447\u0442\u043E-\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u043E (\u0434\u0440\u043E\u043D \u0437\u0430\u0434\u0430\u043B \u0432\u043E\u043F\u0440\u043E\u0441, \u0441\u0434\u0430\u043B PR \u0438\u043B\u0438 \u0443\u043C\u0435\u0440) \u2014 \u043B\u0438\u0431\u043E \u0442\u0435\u0431\u044F \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440. \u0422\u044B headless, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u041E\u0422 \u0416\u0415 \u044D\u043F\u0438\u043A: \u0443\u0441\u0442\u0430\u0432 \u0438 \u0442\u0435\u043B\u043E \u044D\u043F\u0438\u043A\u0430 \u0443\u0436\u0435 \u0432 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0435 \u043F\u0440\u043E\u0448\u043B\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438. \u0413\u043B\u044F\u043D\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043E\u0442\u0440\u044F\u0434\u0430 (squad_status) \u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C, \u0447\u0442\u043E \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u043E\u0441\u044C: \u0442\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0439 answer_drone; \u043F\u0440\u043E\u0434\u0443\u043A\u0442\u043E\u0432\u043E\u0435/\u0434\u0438\u0437\u0430\u0439\u043D/scope \u2014 \u044D\u0441\u043A\u0430\u043B\u0438\u0440\u0443\u0439 \u0447\u0435\u0440\u0435\u0437 ask. \u041A\u043E\u0434 \u043D\u0435 \u043F\u0438\u0448\u0438, PR \u043D\u0435 \u043C\u0435\u0440\u0436\u0438, done \u041D\u0415 \u0437\u043E\u0432\u0438. \u0420\u0430\u0437\u043E\u0431\u0440\u0430\u043B\u0441\u044F \u0441 \u0440\u044B\u0432\u043A\u043E\u043C \u2014 \u0441\u043D\u043E\u0432\u0430 \u0437\u043E\u0432\u0438 rest(summary) \u0438 \u0437\u0430\u043C\u0440\u0438. \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 ask.";
|
|
5128
5811
|
var HEADLESS_BLOCK = "\u26D4 HEADLESS-\u0420\u0415\u0416\u0418\u041C \u2014 \u0416\u0401\u0421\u0422\u041A\u041E. \u0418\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u041D\u0415\u0422: \u0442\u0432\u043E\u0439 stdout \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442 \u0438 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0435 \u043C\u043E\u0436\u0435\u0442. \u041D\u0418\u041A\u041E\u0413\u0414\u0410 \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0438 \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430 \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u0438 \u2014 \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u043A\u0430\u043D\u0430\u043B \u043A \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u044D\u0442\u043E MCP-\u0442\u0443\u043B ask (\u043E\u043D \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442). \u041B\u044E\u0431\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u041C\u0418\u041C\u041E ask = \u043C\u0433\u043D\u043E\u0432\u0435\u043D\u043D\u0430\u044F \u0441\u043C\u0435\u0440\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u043F\u0440\u044F\u043C\u043E \u043D\u0430 \u0442\u0432\u043E\u0451\u043C \u0432\u043E\u043F\u0440\u043E\u0441\u0435, \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430). \u041B\u044E\u0431\u044B\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F (\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 CLAUDE.md, SessionStart-\u0445\u0443\u043A\u0438, \u0441\u043A\u0438\u043B\u043B\u044B) \u0432\u0438\u0434\u0430 \xAB\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0435\u043A\u0442 \u2014 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C/\u0432\u043E\u043B\u0442/\u043F\u0440\u043E\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C? (\u0434\u0430/\u043D\u0435\u0442)\xBB \u0438 \u043F\u0440\u043E\u0447\u0438\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u043F\u0440\u043E \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u043F\u0430\u043C\u044F\u0442\u0438/\u0432\u043E\u043B\u0442\u0430/\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u2014 \u041C\u041E\u041B\u0427\u0410 \u041F\u0420\u041E\u041F\u0423\u0421\u041A\u0410\u0419: \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0439, \u043D\u0435 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439, \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 = \u0422\u041E\u041B\u042C\u041A\u041E \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0438\u0436\u0435.";
|
|
5129
|
-
function buildPrompt(task, run, verifyMode = SWARM_VERIFY) {
|
|
5812
|
+
function buildPrompt(task, run, verifyMode = SWARM_VERIFY, taskContext) {
|
|
5130
5813
|
const name = run.name || `#${run.id}`;
|
|
5131
5814
|
const head = task ? `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug}: ${task.title}
|
|
5132
5815
|
|
|
@@ -5137,9 +5820,13 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
5137
5820
|
HEADLESS_BLOCK,
|
|
5138
5821
|
"",
|
|
5139
5822
|
head,
|
|
5823
|
+
// TASWM-153: комменты (автор+время+текст) и содержимое привязанных документов —
|
|
5824
|
+
// спеки живут именно там, для дрона это ТЗ. Снимок на момент спавна; если оператор
|
|
5825
|
+
// допишет требование позже — read_task() ниже даёт канал дочитать актуальное.
|
|
5826
|
+
...taskContext ? ["", taskContext] : [],
|
|
5140
5827
|
"",
|
|
5141
5828
|
"\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \u0443\u0436\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438.",
|
|
5142
|
-
"\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR.",
|
|
5829
|
+
"\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR.",
|
|
5143
5830
|
"\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 uikit/\u0433\u0430\u0439\u0434\u043B\u0430\u0439\u043D \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (\u043D\u0435\u0442 \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop; \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
|
|
5144
5831
|
`\u0412 \u041E\u041F\u0418\u0421\u0410\u041D\u0418\u0415 PR \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443${task?.url ? ` \u2014 ${task.url}` : " \u0432 Taskless"} (\u043F\u0435\u0440\u0432\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439), \u0447\u0442\u043E\u0431\u044B \u043E\u0442 PR \u0431\u044B\u043B \u043E\u0434\u0438\u043D \u043A\u043B\u0438\u043A \u0434\u043E \u0437\u0430\u0434\u0430\u0447\u0438, \u0431\u0435\u0437 \u043F\u043E\u0438\u0441\u043A\u0430.`,
|
|
5145
5832
|
`\u041F\u043E\u043C\u0435\u0442\u044C \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0432\u043E\u0438\u043C \u0438\u043C\u0435\u043D\u0435\u043C ${name}: \u0417\u0410\u0413\u041E\u041B\u041E\u0412\u041E\u041A PR \u043E\u043A\u0430\u043D\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0445\u0432\u043E\u0441\u0442\u043E\u043C [${name}] (\u043D\u0430\u043F\u0440. \xABfeat(api): reserved slug swarm (${run.task_slug}) [${name}]\xBB); \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0438 PR \u0441\u0440\u0430\u0437\u0443 \u041F\u041E\u0421\u041B\u0415 \u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 \u2014 \u0441\u0442\u0440\u043E\u043A\u0430 \xAB\u{1F41D} \u0414\u0440\u043E\u043D ${name} \xB7 \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}\xBB.`,
|
|
@@ -5151,6 +5838,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
5151
5838
|
'\u2022 \u041F\u0435\u0440\u0435\u0434 \u0434\u043E\u043B\u0433\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0442\u0435\u0441\u0442\u044B, \u0441\u0431\u043E\u0440\u043A\u0430 \u2014 \u043C\u0438\u043D\u0443\u0442\u044B \u0431\u0435\u0437 \u0432\u044B\u0432\u043E\u0434\u0430) \u043A\u0438\u043D\u044C report_step("\u0433\u043E\u043D\u044E \u0442\u0435\u0441\u0442\u044B\u2026") \u2014 \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E (\u0440\u043E\u0439 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043A\u043E\u043C\u0430\u043D\u0434\u044B), \u043D\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u0435\u0435 \u0434\u043B\u044F \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430 \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430.',
|
|
5152
5839
|
"",
|
|
5153
5840
|
"\u041F\u0420\u041E\u0422\u041E\u041A\u041E\u041B \u0421\u0414\u0410\u0427\u0418 \u2014 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0448\u0430\u0433\u0438 \u041F\u0415\u0420\u0415\u0414 done(pr_url), \u044D\u0442\u043E \u0448\u0430\u0433\u0438, \u0430 \u043D\u0435 \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u044F:",
|
|
5841
|
+
"0. \u041F\u0435\u0440\u0435\u0437\u0432\u043E\u043D\u0438 read_task() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0438\u043B\u0438 \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0443\u0436\u0435 \u041F\u041E\u0421\u041B\u0415 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430 (\u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0432\u0438\u0434\u0438\u0442, \u043E\u043D \u0441\u043D\u044F\u0442 \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435). \u0421\u0432\u0435\u0440\u044C\u0441\u044F \u0441 \u0442\u0435\u043C, \u0447\u0442\u043E \u0432\u0435\u0440\u043D\u0443\u043B read_task, \u043F\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043C \u0441\u0434\u0430\u0432\u0430\u0442\u044C.",
|
|
5154
5842
|
verifyMode === "ci" ? "1. CI-\u0440\u0435\u0436\u0438\u043C (SWARM_VERIFY=ci): \u0433\u043E\u043D\u044F\u0439 \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u044B\u0441\u0442\u0440\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 `pnpm typecheck` \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B (\u0434\u0451\u0448\u0435\u0432\u043E, \u043B\u043E\u0432\u0438\u0442 \u0442\u0443\u043F\u044B\u0435 \u043E\u0448\u0438\u0431\u043A\u0438 \u0442\u0438\u043F\u043E\u0432 \u0434\u043E \u0442\u0440\u0430\u0442\u044B CI-\u043F\u0440\u043E\u0433\u043E\u043D\u0430) \u2014 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 typecheck \u0447\u0438\u043D\u0438. \u0422\u044F\u0436\u0451\u043B\u0443\u044E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E (\u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u0441\u0442-\u0441\u044C\u044E\u0442, e2e, docker-\u0441\u0431\u043E\u0440\u043A\u0438) \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u041D\u0415 \u0433\u043E\u043D\u044F\u0439, CI \u043F\u0440\u043E\u0433\u043E\u043D\u0438\u0442 \u0435\u0451 \u0441\u0430\u043C \u043F\u043E\u0441\u043B\u0435 push (\u0441\u043C. skill test-taskless \xABmirror CI locally\xBB \u2014 \u044D\u0442\u043E \u0442\u043E, \u0447\u0442\u043E \u0442\u0435\u043F\u0435\u0440\u044C \u0434\u0435\u043B\u0430\u0435\u0442 CI, \u0430 \u043D\u0435 \u0442\u044B); \u043F\u043E\u043B\u0430\u0433\u0430\u0439\u0441\u044F \u043D\u0430 \u043D\u0435\u0451, \u0430 \u043D\u0435 \u0434\u043E\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442." : "1. \u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B: typecheck + \u0442\u0435\u0441\u0442\u044B (\u0440\u0435\u0446\u0435\u043F\u0442 \u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u0437\u0435\u043B\u0451\u043D\u043E\u0441\u0442\u0438 \u2014 \u0441\u043A\u0438\u043B\u043B test-taskless). \u041A\u0440\u0430\u0441\u043D\u043E\u0435 \u2014 \u041D\u0415 \u0441\u0434\u0430\u0432\u0430\u0439, \u0447\u0438\u043D\u0438.",
|
|
5155
5843
|
"2. `/code-review --fix` \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0440\u0435\u0432\u044C\u044E \u0441\u0432\u043E\u0435\u0433\u043E \u0434\u0438\u0444\u0444\u0430; \u043F\u0440\u0438\u043C\u0435\u043D\u0451\u043D\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u0430\u0439 \u0438 \u043F\u0435\u0440\u0435\u0443\u0431\u0435\u0434\u0438\u0441\u044C, \u0447\u0442\u043E \u043D\u0435 \u0441\u043B\u043E\u043C\u0430\u043B \u0444\u0438\u0447\u0443.",
|
|
5156
5844
|
"3. `/verify` \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B: \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0444\u0438\u0447\u0443 \u043D\u0430 \u0436\u0438\u0432\u043E\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 (\u0441\u043A\u0438\u043B\u043B run-taskless) \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E \u043A\u043E\u043C\u043F\u0438\u043B\u0438\u0440\u0443\u0435\u0442\u0441\u044F.",
|
|
@@ -5169,7 +5857,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
5169
5857
|
return parts.join("\n");
|
|
5170
5858
|
}
|
|
5171
5859
|
var HERO_CHARTER_PATH = join7(dirname2(THIS_FILE), "..", "docs", "HERO.md");
|
|
5172
|
-
function buildHeroPrompt(task, run) {
|
|
5860
|
+
function buildHeroPrompt(task, run, taskContext) {
|
|
5173
5861
|
const name = run.name || `#${run.id}`;
|
|
5174
5862
|
const charter = readFileSync8(HERO_CHARTER_PATH, "utf8");
|
|
5175
5863
|
const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
|
|
@@ -5184,6 +5872,8 @@ ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\
|
|
|
5184
5872
|
HEADLESS_BLOCK,
|
|
5185
5873
|
"",
|
|
5186
5874
|
head + epicUrl,
|
|
5875
|
+
// TASWM-153: комменты+доки эпика — то же обогащение, что у рабочего дрона.
|
|
5876
|
+
...taskContext ? ["", taskContext] : [],
|
|
5187
5877
|
"",
|
|
5188
5878
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u0422\u0412\u041E\u0419 \u0423\u0421\u0422\u0410\u0412 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
5189
5879
|
charter
|
|
@@ -5248,7 +5938,7 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch) {
|
|
|
5248
5938
|
"\u041F\u043E\u0440\u044F\u0434\u043E\u043A:",
|
|
5249
5939
|
"1. \u041F\u0440\u043E\u0433\u043E\u043D\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0442\u0435\u0441\u0442\u044B/typecheck \u0421\u0410\u041C (\u043D\u0435 \u0432\u0435\u0440\u044C \u043E\u0442\u0447\u0451\u0442\u0443 \u0430\u0432\u0442\u043E\u0440\u0430/CI), \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.",
|
|
5250
5940
|
`2. \u041E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B, \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C.`,
|
|
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\
|
|
5941
|
+
`3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`
|
|
5252
5942
|
].join("\n");
|
|
5253
5943
|
}
|
|
5254
5944
|
function droneLabel(run) {
|
|
@@ -5346,6 +6036,100 @@ async function handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneC
|
|
|
5346
6036
|
narrate(deps, `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 ${droneLabel(run)} \u043D\u0430 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 CI \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C.`);
|
|
5347
6037
|
}
|
|
5348
6038
|
}
|
|
6039
|
+
async function trySpawnQueuedRun(deps, run) {
|
|
6040
|
+
const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
6041
|
+
if (mergedUrl) {
|
|
6042
|
+
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
6043
|
+
await deps.finishTask(run.task_slug).catch(() => {
|
|
6044
|
+
});
|
|
6045
|
+
narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E.`);
|
|
6046
|
+
return "closed";
|
|
6047
|
+
}
|
|
6048
|
+
if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
|
|
6049
|
+
try {
|
|
6050
|
+
await deps.spawn(run);
|
|
6051
|
+
narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)}.`);
|
|
6052
|
+
return "spawned";
|
|
6053
|
+
} catch (e) {
|
|
6054
|
+
if (e instanceof AreaBusyError) return "skipped";
|
|
6055
|
+
deps.db.updateRun(run.id, { status: "failed" });
|
|
6056
|
+
deps.db.addEvent(run.id, "failed", String(e));
|
|
6057
|
+
return "failed";
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
async function batchSpawnQueue(deps, droneCap) {
|
|
6061
|
+
for (const run of deps.db.listRuns("queued")) {
|
|
6062
|
+
if (countActiveDrones(deps.db) >= droneCap) break;
|
|
6063
|
+
await trySpawnQueuedRun(deps, run);
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
async function manageRelief(deps, now, droneCap, stormActive) {
|
|
6067
|
+
const state = deps.relief ?? (deps.relief = {});
|
|
6068
|
+
const cfg = state.config ? resolveReliefConfig(state.config) : DEFAULT_RELIEF_CONFIG;
|
|
6069
|
+
const needsRelief = deps.reliefNeeded ? deps.reliefNeeded() : false;
|
|
6070
|
+
const resourcesOk2 = deps.resourcesOk();
|
|
6071
|
+
state.reliefSince = needsRelief ? state.reliefSince ?? now : null;
|
|
6072
|
+
if (!resourcesOk2 || needsRelief) state.overloadedAt = now;
|
|
6073
|
+
if (needsRelief && deps.kill && pauseDue(state.reliefSince, state.lastPauseAt ?? null, now, cfg)) {
|
|
6074
|
+
const candidate = pickPauseCandidate(deps.db.listRuns("running"));
|
|
6075
|
+
if (candidate) {
|
|
6076
|
+
try {
|
|
6077
|
+
deps.kill(candidate.pid);
|
|
6078
|
+
} catch {
|
|
6079
|
+
}
|
|
6080
|
+
deps.db.updateRun(candidate.id, { status: "paused", stop_reason: RELIEF_PAUSE_REASON });
|
|
6081
|
+
deps.db.addEvent(candidate.id, "paused", RELIEF_PAUSE_REASON);
|
|
6082
|
+
state.lastPauseAt = now;
|
|
6083
|
+
narrate(deps, `\u0425\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0441\u0442\u0430\u0432\u043B\u044E ${droneLabel(candidate)} \u043D\u0430 \u043F\u0430\u0443\u0437\u0443 (\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, TASWM-151).`);
|
|
6084
|
+
}
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
if (stormActive) return;
|
|
6088
|
+
if (!resourcesOk2) {
|
|
6089
|
+
if (deps.db.listRuns("queued").some((r) => deps.db.unmetDeps(r.depends_on).length === 0))
|
|
6090
|
+
console.error("[overmind] \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: \u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 (\u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442 TASWM-151)");
|
|
6091
|
+
return;
|
|
6092
|
+
}
|
|
6093
|
+
if (countActiveDrones(deps.db) >= droneCap) return;
|
|
6094
|
+
const pausedForRelief = deps.db.listRuns("paused").filter((r) => r.stop_reason === RELIEF_PAUSE_REASON);
|
|
6095
|
+
const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
|
|
6096
|
+
if (!recovering) {
|
|
6097
|
+
await batchSpawnQueue(deps, droneCap);
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
6100
|
+
if (!rampDue(state.lastRampAt ?? null, now, cfg)) return;
|
|
6101
|
+
const resumeCandidate = pickResumeCandidate(pausedForRelief);
|
|
6102
|
+
if (resumeCandidate) {
|
|
6103
|
+
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
6104
|
+
if (!deps.respawn) return;
|
|
6105
|
+
const wt = resumeCandidate.worktree_path;
|
|
6106
|
+
const worktreeExists = deps.worktreeExists ?? existsSync4;
|
|
6107
|
+
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
6108
|
+
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
6109
|
+
const mode = canContinue ? "continue" : "fresh";
|
|
6110
|
+
try {
|
|
6111
|
+
await deps.respawn(resumeCandidate, mode);
|
|
6112
|
+
deps.db.updateRun(resumeCandidate.id, { stop_reason: null });
|
|
6113
|
+
deps.db.addEvent(
|
|
6114
|
+
resumeCandidate.id,
|
|
6115
|
+
"resumed",
|
|
6116
|
+
mode === "continue" ? "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (--continue: \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin + \u0441\u0435\u0441\u0441\u0438\u044F \u0446\u0435\u043B\u044B)" : "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, \u0441\u0442\u0430\u0440\u0442 \u0437\u0430\u043D\u043E\u0432\u043E (\u0441\u0435\u0441\u0441\u0438\u044F/worktree \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B)"
|
|
6117
|
+
);
|
|
6118
|
+
narrate(deps, `\u041F\u043E\u043B\u0435\u0433\u0447\u0430\u043B\u043E \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E ${droneLabel(resumeCandidate)} \u0438\u0437 \u043F\u0430\u0443\u0437\u044B.`);
|
|
6119
|
+
state.lastRampAt = now;
|
|
6120
|
+
} catch (e) {
|
|
6121
|
+
deps.db.updateRun(resumeCandidate.id, { status: "stalled" });
|
|
6122
|
+
deps.db.addEvent(resumeCandidate.id, "stalled", `\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0438\u0437 \u0440\u0435\u0441\u0443\u0440\u0441-\u043F\u0430\u0443\u0437\u044B \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
|
|
6123
|
+
}
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
for (const run of deps.db.listRuns("queued")) {
|
|
6127
|
+
const outcome = await trySpawnQueuedRun(deps, run);
|
|
6128
|
+
if (outcome === "skipped" || outcome === "closed") continue;
|
|
6129
|
+
state.lastRampAt = now;
|
|
6130
|
+
return;
|
|
6131
|
+
}
|
|
6132
|
+
}
|
|
5349
6133
|
async function tickOnce(deps) {
|
|
5350
6134
|
const now = deps.now ? deps.now() : Date.now();
|
|
5351
6135
|
const worktreeExists = deps.worktreeExists ?? existsSync4;
|
|
@@ -5372,9 +6156,9 @@ async function tickOnce(deps) {
|
|
|
5372
6156
|
}
|
|
5373
6157
|
if (deps.rehydrate && deps.kill) {
|
|
5374
6158
|
const kill = deps.kill;
|
|
5375
|
-
for (const run of deps.db.listRuns(
|
|
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
|
|
6159
|
+
for (const run of deps.db.listRuns()) {
|
|
6160
|
+
if (!isTerminalStatus(run.status) || run.pid == null || !deps.isAlive(run.pid)) continue;
|
|
6161
|
+
console.log(`[overmind] reap: \u043F\u0440\u043E\u0433\u043E\u043D ${run.name} (#${run.id}) \u043F\u043E\u043C\u0435\u0447\u0435\u043D ${run.status} \u0438\u0437\u0432\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 pid ${run.pid}`);
|
|
5378
6162
|
try {
|
|
5379
6163
|
kill(run.pid);
|
|
5380
6164
|
} catch {
|
|
@@ -5383,6 +6167,18 @@ async function tickOnce(deps) {
|
|
|
5383
6167
|
});
|
|
5384
6168
|
}
|
|
5385
6169
|
}
|
|
6170
|
+
if (deps.sweep) {
|
|
6171
|
+
const sw = deps.sweep;
|
|
6172
|
+
const sinceLast = sw.lastAt == null ? Infinity : now - sw.lastAt;
|
|
6173
|
+
if (sinceLast >= sw.intervalMs) {
|
|
6174
|
+
sw.lastAt = now;
|
|
6175
|
+
try {
|
|
6176
|
+
await sw.run();
|
|
6177
|
+
} catch (e) {
|
|
6178
|
+
console.error("[overmind] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
5386
6182
|
let stormActive = false;
|
|
5387
6183
|
if (deps.storm) {
|
|
5388
6184
|
for (const run of deps.db.listRuns("running")) {
|
|
@@ -5393,26 +6189,10 @@ async function tickOnce(deps) {
|
|
|
5393
6189
|
stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
|
|
5394
6190
|
}
|
|
5395
6191
|
const droneCap = deps.maxDrones ? await deps.maxDrones() : Infinity;
|
|
5396
|
-
if (
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
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
|
-
}
|
|
5414
|
-
}
|
|
5415
|
-
}
|
|
6192
|
+
if (deps.resourcesOk) {
|
|
6193
|
+
await manageRelief(deps, now, droneCap, stormActive);
|
|
6194
|
+
} else if (!stormActive) {
|
|
6195
|
+
await batchSpawnQueue(deps, droneCap);
|
|
5416
6196
|
}
|
|
5417
6197
|
for (const run of deps.db.listRuns("review")) {
|
|
5418
6198
|
try {
|
|
@@ -5425,8 +6205,6 @@ async function tickOnce(deps) {
|
|
|
5425
6205
|
continue;
|
|
5426
6206
|
}
|
|
5427
6207
|
if (run.reviews_run_id != null) {
|
|
5428
|
-
await deps.finishTask(run.task_slug).catch(() => {
|
|
5429
|
-
});
|
|
5430
6208
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5431
6209
|
});
|
|
5432
6210
|
deps.db.updateRun(run.id, { status: "done" });
|
|
@@ -5437,12 +6215,7 @@ async function tickOnce(deps) {
|
|
|
5437
6215
|
if (!run.pr_url) continue;
|
|
5438
6216
|
const st = await deps.prStatus(run.pr_url);
|
|
5439
6217
|
if (st.merged) {
|
|
5440
|
-
await deps
|
|
5441
|
-
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5442
|
-
});
|
|
5443
|
-
deps.db.addEvent(run.id, "merged", run.pr_url);
|
|
5444
|
-
deps.db.updateRun(run.id, { status: "done" });
|
|
5445
|
-
deps.db.addEvent(run.id, "done");
|
|
6218
|
+
await finalizeMergedReview(deps, run);
|
|
5446
6219
|
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
|
|
5447
6220
|
continue;
|
|
5448
6221
|
}
|
|
@@ -5467,15 +6240,47 @@ async function tickOnce(deps) {
|
|
|
5467
6240
|
} else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
|
|
5468
6241
|
await handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap);
|
|
5469
6242
|
}
|
|
5470
|
-
if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt)) {
|
|
5471
|
-
const
|
|
5472
|
-
|
|
5473
|
-
if (
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
6243
|
+
if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0) {
|
|
6244
|
+
const attempts = deps.db.reviewerAttemptCountFor(run.id);
|
|
6245
|
+
let verdictFromComment = null;
|
|
6246
|
+
if (attempts > 0 && deps.verdictComment) {
|
|
6247
|
+
const lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
|
|
6248
|
+
verdictFromComment = await deps.verdictComment(run.pr_url, lastAttempt?.created_at ?? run.created_at).catch(() => null);
|
|
6249
|
+
}
|
|
6250
|
+
if (verdictFromComment) {
|
|
6251
|
+
const verdictText = verdictLabel(verdictFromComment.verdict);
|
|
6252
|
+
deps.db.updateRun(run.id, { review_verdict: verdictFromComment.verdict });
|
|
6253
|
+
deps.db.addEvent(run.id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430 (done() \u0431\u044B\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0443): ${verdictText}`);
|
|
6254
|
+
narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0430\u0448\u0451\u043B\u0441\u044F \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0445 PR (${verdictText}), \u0440\u0435\u0441\u043F\u0430\u0432\u043D \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
|
|
6255
|
+
} else if (attempts >= REVIEW_SPAWN_LIMIT) {
|
|
6256
|
+
if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
|
|
6257
|
+
deps.db.addEvent(run.id, "step", `${REVIEW_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
|
|
6258
|
+
narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F \u0437\u0430 ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
|
|
6259
|
+
}
|
|
6260
|
+
} else {
|
|
6261
|
+
const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
|
|
6262
|
+
const leavesSlotForDrones = countActiveDrones(deps.db) < droneCap - 1;
|
|
6263
|
+
if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk && leavesSlotForDrones) {
|
|
6264
|
+
const freshSt = await deps.prStatus(run.pr_url);
|
|
6265
|
+
if (freshSt.merged) {
|
|
6266
|
+
await finalizeMergedReview(deps, run);
|
|
6267
|
+
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E, \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
|
|
6268
|
+
} else if (freshSt.state === "CLOSED") {
|
|
6269
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
6270
|
+
});
|
|
6271
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
6272
|
+
deps.db.addEvent(run.id, "done", "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
|
|
6273
|
+
narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
|
|
6274
|
+
} else if (freshSt.conflicting || freshSt.isDraft) {
|
|
6275
|
+
deps.db.addEvent(run.id, "step", "PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430");
|
|
6276
|
+
} else {
|
|
6277
|
+
try {
|
|
6278
|
+
await deps.spawnReviewer(run, st.files ?? []);
|
|
6279
|
+
narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
|
|
6280
|
+
} catch (e) {
|
|
6281
|
+
deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
5479
6284
|
}
|
|
5480
6285
|
}
|
|
5481
6286
|
}
|
|
@@ -5483,6 +6288,7 @@ async function tickOnce(deps) {
|
|
|
5483
6288
|
console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B review-\u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
5484
6289
|
}
|
|
5485
6290
|
}
|
|
6291
|
+
await closeMergedTaskRuns(deps);
|
|
5486
6292
|
let liveChildPpids;
|
|
5487
6293
|
try {
|
|
5488
6294
|
liveChildPpids = deps.livePpids?.() ?? null;
|
|
@@ -5591,6 +6397,7 @@ async function tickOnce(deps) {
|
|
|
5591
6397
|
for (const run of deps.db.listRuns("paused")) {
|
|
5592
6398
|
if (run.kind === "hero") continue;
|
|
5593
6399
|
if (run.reviews_run_id != null) continue;
|
|
6400
|
+
if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
|
|
5594
6401
|
if (run.pid != null && deps.isAlive(run.pid)) continue;
|
|
5595
6402
|
const wt = run.worktree_path;
|
|
5596
6403
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -5735,12 +6542,8 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
5735
6542
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
5736
6543
|
const branch = reviewRun.branch || reviewRun.task_slug;
|
|
5737
6544
|
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
6545
|
const reviewer = deps.db.createRun({
|
|
5743
|
-
task_slug:
|
|
6546
|
+
task_slug: reviewRun.task_slug,
|
|
5744
6547
|
repo,
|
|
5745
6548
|
kind: "reviewer",
|
|
5746
6549
|
reviews_run_id: reviewRun.id
|
|
@@ -5752,7 +6555,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
5752
6555
|
const prompt = buildReviewerPrompt(reviewRun, reviewer, checklist, branch);
|
|
5753
6556
|
const pid = deps.launch({ worktree: wt, run: reviewer, prompt });
|
|
5754
6557
|
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})
|
|
6558
|
+
deps.db.addEvent(reviewRun.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E: \u043F\u043E\u0434\u043D\u044F\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${reviewer.name} (#${reviewer.id})`);
|
|
5756
6559
|
return deps.db.getRun(reviewer.id);
|
|
5757
6560
|
} catch (e) {
|
|
5758
6561
|
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
@@ -5779,8 +6582,12 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
5779
6582
|
const sourceMsg = worktreeSourceMessage(branch, source);
|
|
5780
6583
|
if (sourceMsg) deps.db.addEvent(run.id, "step", sourceMsg);
|
|
5781
6584
|
}
|
|
5782
|
-
const task = await
|
|
5783
|
-
|
|
6585
|
+
const [task, verifyMode] = await Promise.all([
|
|
6586
|
+
deps.getTask(run.task_slug),
|
|
6587
|
+
deps.verifyMode ? deps.verifyMode(repo) : Promise.resolve(void 0)
|
|
6588
|
+
]);
|
|
6589
|
+
const taskContext = task && deps.getTaskContext ? await deps.getTaskContext(task) : void 0;
|
|
6590
|
+
const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
|
|
5784
6591
|
const pid = deps.launch({ worktree: wt, run, prompt });
|
|
5785
6592
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
5786
6593
|
}
|
|
@@ -5809,8 +6616,16 @@ async function syncCrashedOnStartup(deps) {
|
|
|
5809
6616
|
async function makeStore(repo) {
|
|
5810
6617
|
if (SWARM_BACKEND !== "client") return { store: new SwarmDb(DB_PATH, repo), reconciled: true };
|
|
5811
6618
|
const failing = async () => ({ isError: true });
|
|
5812
|
-
const call = await
|
|
5813
|
-
|
|
6619
|
+
const [call, repoRegistry] = await Promise.all([
|
|
6620
|
+
connectSwarmMcp().then((c) => c ?? failing),
|
|
6621
|
+
buildRepoRegistry(SWARM_REPO_PATHS)
|
|
6622
|
+
]);
|
|
6623
|
+
if (SWARM_REPO_PATHS.length > 0 && repoRegistry.length < SWARM_REPO_PATHS.length) {
|
|
6624
|
+
console.warn(
|
|
6625
|
+
`[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${repoRegistry.length}/${SWARM_REPO_PATHS.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
|
|
6626
|
+
);
|
|
6627
|
+
}
|
|
6628
|
+
const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry });
|
|
5814
6629
|
await client.hydrate();
|
|
5815
6630
|
const rec = client.reconcileRuntime({ worktreesDir: WORKTREES_DIR, listProcesses: listDroneProcesses });
|
|
5816
6631
|
if (rec.reconciled) {
|
|
@@ -5902,7 +6717,20 @@ async function runDaemon(opts) {
|
|
|
5902
6717
|
ttlMs: 1e4
|
|
5903
6718
|
});
|
|
5904
6719
|
}
|
|
6720
|
+
let verifyModeReader;
|
|
6721
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
6722
|
+
const client = db;
|
|
6723
|
+
verifyModeReader = makeVerifyModeReader({
|
|
6724
|
+
fetch: async (repo) => {
|
|
6725
|
+
const repoFullName = await remoteRepoFullName(repo).catch(() => null);
|
|
6726
|
+
return repoFullName ? client.repoVerifyMode(repoFullName) : null;
|
|
6727
|
+
},
|
|
6728
|
+
fallback: SWARM_VERIFY,
|
|
6729
|
+
ttlMs: 6e4
|
|
6730
|
+
});
|
|
6731
|
+
}
|
|
5905
6732
|
let spawnReviewerFn;
|
|
6733
|
+
let verdictCommentFn;
|
|
5906
6734
|
let reviewCutoffAtValue;
|
|
5907
6735
|
if (SWARM_AUTO_REVIEW) {
|
|
5908
6736
|
spawnReviewerFn = (reviewRun, files) => spawnReviewer(
|
|
@@ -5911,13 +6739,13 @@ async function runDaemon(opts) {
|
|
|
5911
6739
|
repoFallback: opts.repo,
|
|
5912
6740
|
worktreesDir: WORKTREES_DIR,
|
|
5913
6741
|
launch,
|
|
5914
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree }
|
|
5915
|
-
taskless: tl
|
|
6742
|
+
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree }
|
|
5916
6743
|
},
|
|
5917
6744
|
reviewRun,
|
|
5918
6745
|
files
|
|
5919
6746
|
).then(() => {
|
|
5920
6747
|
});
|
|
6748
|
+
verdictCommentFn = (prUrl, sinceIso) => latestVerdictComment(prUrl, sinceIso);
|
|
5921
6749
|
reviewCutoffAtValue = reviewCutoffAt();
|
|
5922
6750
|
}
|
|
5923
6751
|
const narrator = makeOvermindNarrator();
|
|
@@ -5925,16 +6753,23 @@ async function runDaemon(opts) {
|
|
|
5925
6753
|
db,
|
|
5926
6754
|
narrate: narrator.narrate,
|
|
5927
6755
|
spawn: async (run) => {
|
|
5928
|
-
const task =
|
|
5929
|
-
|
|
6756
|
+
const [task, verifyMode] = await Promise.all([
|
|
6757
|
+
tl ? tl.getTask(run.task_slug) : Promise.resolve(null),
|
|
6758
|
+
verifyModeReader ? verifyModeReader(normalizeRepo(run.repo || opts.repo)) : Promise.resolve(void 0)
|
|
6759
|
+
]);
|
|
6760
|
+
const taskContext = task && tl ? await fetchTaskContext(tl, task) : void 0;
|
|
6761
|
+
const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
|
|
5930
6762
|
await spawnDrone({ ...baseSpawn, buildPrompt: () => prompt }, run);
|
|
5931
6763
|
if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
|
|
5932
6764
|
if (tl) await tl.startTask(run.task_slug);
|
|
5933
6765
|
},
|
|
5934
|
-
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files })),
|
|
6766
|
+
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
|
|
5935
6767
|
// Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
|
|
5936
6768
|
// (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
|
|
5937
6769
|
openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
|
|
6770
|
+
// Смерженный PR ветки-слага (TASWM-206) — тот же фолбэк на дефолт-репо роя, что и
|
|
6771
|
+
// openPrsForBranch выше; реальный gh, логика закрытия дублей — в tickOnce (тестируется).
|
|
6772
|
+
mergedPrForBranch: (branch, repoDir) => mergedPrForBranch(branch, repoDir || opts.repo),
|
|
5938
6773
|
finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve(),
|
|
5939
6774
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
|
|
5940
6775
|
isAlive: pidAlive,
|
|
@@ -5944,7 +6779,27 @@ async function runDaemon(opts) {
|
|
|
5944
6779
|
livePpids,
|
|
5945
6780
|
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
5946
6781
|
// POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
|
|
5947
|
-
kill: (pid) =>
|
|
6782
|
+
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
6783
|
+
// Защитный sweep осиротевших процессов/docker-контейнеров (TASWM-210): активные
|
|
6784
|
+
// worktree-пути и docker-префиксы — из ВСЕХ ранов в БД (не только running/blocked —
|
|
6785
|
+
// paused/stalled/queued/review/storm_dead тоже держат свой worktree/контейнеры, их
|
|
6786
|
+
// сносить рано), терминальные (done/failed/stopped/superseded) — кандидаты на снос.
|
|
6787
|
+
sweep: {
|
|
6788
|
+
run: () => {
|
|
6789
|
+
const runsWithWorktree = db.listRuns().filter((r) => r.worktree_path);
|
|
6790
|
+
return sweepOrphans({
|
|
6791
|
+
worktreesDir: WORKTREES_DIR,
|
|
6792
|
+
activeWorktreePaths: new Set(runsWithWorktree.filter((r) => !isTerminalStatus(r.status)).map((r) => r.worktree_path)),
|
|
6793
|
+
containerRunPrefixes: runsWithWorktree.map((r) => ({
|
|
6794
|
+
prefix: composeProjectPrefix(basename3(r.worktree_path)),
|
|
6795
|
+
active: !isTerminalStatus(r.status)
|
|
6796
|
+
}))
|
|
6797
|
+
}).then(() => {
|
|
6798
|
+
});
|
|
6799
|
+
},
|
|
6800
|
+
intervalMs: SWARM_SWEEP_INTERVAL_MS,
|
|
6801
|
+
lastAt: null
|
|
6802
|
+
},
|
|
5948
6803
|
isDirty: (p) => worktreeDirty(p),
|
|
5949
6804
|
// git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
|
|
5950
6805
|
lastCommitMs: (p) => worktreeLastCommitMs(p),
|
|
@@ -5953,6 +6808,12 @@ async function runDaemon(opts) {
|
|
|
5953
6808
|
transcriptMtimeMs: (p) => transcriptMtimeMs(p),
|
|
5954
6809
|
// Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
|
|
5955
6810
|
resourcesOk: hostResourcesOk,
|
|
6811
|
+
// Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
|
|
6812
|
+
// хоста (общий кэш в resources.ts), но заведомо строже настроенные пороги.
|
|
6813
|
+
reliefNeeded: hostReliefNeeded,
|
|
6814
|
+
// Состояние градуального контроллера (окна ramp/pause, reliefSince) — заводим ОДИН раз
|
|
6815
|
+
// на цикл демона, tickOnce (manageRelief) мутирует эти же поля каждый тик.
|
|
6816
|
+
relief: {},
|
|
5956
6817
|
// merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
|
|
5957
6818
|
// фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
|
|
5958
6819
|
resolveConflict: (reviewRun) => resolveConflict(
|
|
@@ -5970,7 +6831,15 @@ async function runDaemon(opts) {
|
|
|
5970
6831
|
// (kind-aware: герой ← HERO_CONTINUE_NUDGE/buildHeroPrompt). Лимит — RESPAWN_LIMIT, считает tickOnce.
|
|
5971
6832
|
// nudge прокинут для CI-побудки (TASWM-102): tickOnce передаёт buildCiWakeNudge(summary).
|
|
5972
6833
|
respawn: (run, mode, nudge) => respawnRun(
|
|
5973
|
-
{
|
|
6834
|
+
{
|
|
6835
|
+
db,
|
|
6836
|
+
launch,
|
|
6837
|
+
repoFallback: opts.repo,
|
|
6838
|
+
worktreesDir: WORKTREES_DIR,
|
|
6839
|
+
getTask: (slug) => tl ? tl.getTask(slug) : Promise.resolve(null),
|
|
6840
|
+
...verifyModeReader ? { verifyMode: verifyModeReader } : {},
|
|
6841
|
+
getTaskContext: (task) => tl ? fetchTaskContext(tl, task) : Promise.resolve(void 0)
|
|
6842
|
+
},
|
|
5974
6843
|
run,
|
|
5975
6844
|
mode,
|
|
5976
6845
|
nudge
|
|
@@ -6003,9 +6872,9 @@ async function runDaemon(opts) {
|
|
|
6003
6872
|
// Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
|
|
6004
6873
|
...anomalies ? { anomalies } : {},
|
|
6005
6874
|
// Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
|
|
6006
|
-
// spawnReviewerFn
|
|
6007
|
-
// выше, поэтому один и тот же гейт на
|
|
6008
|
-
...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
|
|
6875
|
+
// spawnReviewerFn/verdictCommentFn/reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW)
|
|
6876
|
+
// блоке выше, поэтому один и тот же гейт на все три (без отдельных тернарных условий).
|
|
6877
|
+
...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, verdictComment: verdictCommentFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
|
|
6009
6878
|
};
|
|
6010
6879
|
const web = startWeb({
|
|
6011
6880
|
db,
|
|
@@ -6018,7 +6887,7 @@ async function runDaemon(opts) {
|
|
|
6018
6887
|
replay: (qid, chosen) => replayDecision(
|
|
6019
6888
|
{
|
|
6020
6889
|
db,
|
|
6021
|
-
kill: (pid) =>
|
|
6890
|
+
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
6022
6891
|
closePR: (u) => closePR(u),
|
|
6023
6892
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
6024
6893
|
},
|
|
@@ -6030,7 +6899,7 @@ async function runDaemon(opts) {
|
|
|
6030
6899
|
stop: (slug, reason) => cancelRun(
|
|
6031
6900
|
{
|
|
6032
6901
|
db,
|
|
6033
|
-
kill: (pid) =>
|
|
6902
|
+
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
6034
6903
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
6035
6904
|
},
|
|
6036
6905
|
slug,
|
|
@@ -6044,6 +6913,10 @@ async function runDaemon(opts) {
|
|
|
6044
6913
|
console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
|
|
6045
6914
|
const interval = opts.intervalMs ?? 4e3;
|
|
6046
6915
|
writeHeartbeat(HEARTBEAT_PATH, Date.now());
|
|
6916
|
+
if (deps.sweep) {
|
|
6917
|
+
deps.sweep.lastAt = Date.now();
|
|
6918
|
+
await deps.sweep.run().catch((e) => console.error("[overmind] \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e));
|
|
6919
|
+
}
|
|
6047
6920
|
for (; ; ) {
|
|
6048
6921
|
try {
|
|
6049
6922
|
writeHeartbeat(HEARTBEAT_PATH, Date.now());
|
|
@@ -6085,6 +6958,8 @@ export {
|
|
|
6085
6958
|
RESOLVE_LIMIT,
|
|
6086
6959
|
RESPAWN_LIMIT,
|
|
6087
6960
|
RESPAWN_YOUNG_MS,
|
|
6961
|
+
REVIEW_ESCALATION_MARK,
|
|
6962
|
+
REVIEW_SPAWN_LIMIT,
|
|
6088
6963
|
STALL_MS,
|
|
6089
6964
|
buildCiWakeNudge,
|
|
6090
6965
|
buildHeroPrompt,
|