@taskless-app/swarm 0.6.19 → 0.6.21
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 +68 -8
- package/dist/overmind.js +523 -36
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2167,7 +2167,7 @@ function tokenFromFile() {
|
|
|
2167
2167
|
return "";
|
|
2168
2168
|
}
|
|
2169
2169
|
}
|
|
2170
|
-
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, 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, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
|
|
2170
|
+
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, SWARM_WORKTREE_REAP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, 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, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
|
|
2171
2171
|
var init_config = __esm({
|
|
2172
2172
|
"src/config.ts"() {
|
|
2173
2173
|
"use strict";
|
|
@@ -2216,6 +2216,7 @@ var init_config = __esm({
|
|
|
2216
2216
|
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
2217
2217
|
SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
|
|
2218
2218
|
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
2219
|
+
SWARM_WORKTREE_REAP_INTERVAL_MS = envNum("SWARM_WORKTREE_REAP_INTERVAL_MS", 5 * 6e4);
|
|
2219
2220
|
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
2220
2221
|
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
2221
2222
|
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
@@ -2372,7 +2373,7 @@ var init_package = __esm({
|
|
|
2372
2373
|
"package.json"() {
|
|
2373
2374
|
package_default = {
|
|
2374
2375
|
name: "@taskless-app/swarm",
|
|
2375
|
-
version: "0.6.
|
|
2376
|
+
version: "0.6.21",
|
|
2376
2377
|
type: "module",
|
|
2377
2378
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
2378
2379
|
keywords: [
|
|
@@ -2422,7 +2423,7 @@ var init_package = __esm({
|
|
|
2422
2423
|
execa: "^10.0.0",
|
|
2423
2424
|
zod: "3.25.76"
|
|
2424
2425
|
},
|
|
2425
|
-
gitHead: "
|
|
2426
|
+
gitHead: "584411cbe69583a8727e89caa8ae7e626f8fd8ca"
|
|
2426
2427
|
};
|
|
2427
2428
|
}
|
|
2428
2429
|
});
|
|
@@ -8509,10 +8510,24 @@ async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
|
8509
8510
|
}
|
|
8510
8511
|
}
|
|
8511
8512
|
var NO_CHECKS_STDERR = /no checks reported/i;
|
|
8513
|
+
var CiAccessDeniedError = class extends Error {
|
|
8514
|
+
detail;
|
|
8515
|
+
constructor(detail) {
|
|
8516
|
+
super(`\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C: ${detail}`);
|
|
8517
|
+
this.name = "CiAccessDeniedError";
|
|
8518
|
+
this.detail = detail;
|
|
8519
|
+
}
|
|
8520
|
+
};
|
|
8521
|
+
var DENIED_STDERR = /(?:^|\D)403(?:\D|$)|not accessible by integration|must have admin rights|resource not accessible/i;
|
|
8522
|
+
function firstLine(s) {
|
|
8523
|
+
return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "gh \u0432\u0435\u0440\u043D\u0443\u043B 403";
|
|
8524
|
+
}
|
|
8512
8525
|
var realChecksRunner = async (pr) => {
|
|
8513
8526
|
const r = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
|
|
8514
8527
|
const stdout = r.stdout ?? "";
|
|
8515
|
-
|
|
8528
|
+
const stderr = r.stderr ?? "";
|
|
8529
|
+
if (!stdout.trim() && NO_CHECKS_STDERR.test(stderr)) return "[]";
|
|
8530
|
+
if (!stdout.trim() && DENIED_STDERR.test(stderr)) throw new CiAccessDeniedError(firstLine(stderr));
|
|
8516
8531
|
return stdout;
|
|
8517
8532
|
};
|
|
8518
8533
|
async function prCheckRows(pr, run) {
|
|
@@ -8521,13 +8536,17 @@ async function prCheckRows(pr, run) {
|
|
|
8521
8536
|
const rows = JSON.parse(out);
|
|
8522
8537
|
if (!Array.isArray(rows)) return null;
|
|
8523
8538
|
return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
|
|
8524
|
-
} catch {
|
|
8539
|
+
} catch (e) {
|
|
8540
|
+
if (e instanceof CiAccessDeniedError) return { denied: e.detail };
|
|
8525
8541
|
return null;
|
|
8526
8542
|
}
|
|
8527
8543
|
}
|
|
8544
|
+
function isDenied(rows) {
|
|
8545
|
+
return rows != null && !Array.isArray(rows);
|
|
8546
|
+
}
|
|
8528
8547
|
async function ghChecks(pr, run = realChecksRunner) {
|
|
8529
8548
|
const rows = await prCheckRows(pr, run);
|
|
8530
|
-
if (!rows || rows.length === 0) return null;
|
|
8549
|
+
if (!rows || isDenied(rows) || rows.length === 0) return null;
|
|
8531
8550
|
let pass = 0, fail = 0, pending2 = 0;
|
|
8532
8551
|
for (const r of rows) {
|
|
8533
8552
|
if (r.bucket === "pass") pass++;
|
|
@@ -11022,12 +11041,15 @@ async function previousHooksPath(worktree) {
|
|
|
11022
11041
|
}
|
|
11023
11042
|
async function removeWorktree(repo, path) {
|
|
11024
11043
|
const branch = (await $5({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
11044
|
+
await releaseWorktree(repo, path);
|
|
11045
|
+
if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
11046
|
+
}
|
|
11047
|
+
async function releaseWorktree(repo, path) {
|
|
11025
11048
|
await cleanupWorktreeProcesses(path);
|
|
11026
11049
|
await detachWorktree(repo, path);
|
|
11027
|
-
if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
11028
11050
|
}
|
|
11029
11051
|
async function worktreeDirty(path) {
|
|
11030
|
-
const st = await $5({ reject: false })`git -C ${path} status --porcelain`;
|
|
11052
|
+
const st = await $5({ reject: false })`git -C ${path} status --porcelain --untracked-files=all`;
|
|
11031
11053
|
if (st.exitCode !== 0) return true;
|
|
11032
11054
|
const status = st.stdout.trim();
|
|
11033
11055
|
if (status) return true;
|
|
@@ -11080,6 +11102,44 @@ init_config();
|
|
|
11080
11102
|
init_agentToken();
|
|
11081
11103
|
init_linkFailure();
|
|
11082
11104
|
|
|
11105
|
+
// src/toolFailure.ts
|
|
11106
|
+
init_linkFailure();
|
|
11107
|
+
var TOOL_FAILURE = {
|
|
11108
|
+
/** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
|
|
11109
|
+
NOT_FOUND: "not_found",
|
|
11110
|
+
/** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
|
|
11111
|
+
FORBIDDEN: "forbidden",
|
|
11112
|
+
/** Токен роя не состоит в воркспейсе, которым искали. */
|
|
11113
|
+
WORKSPACE_MISMATCH: "workspace_mismatch",
|
|
11114
|
+
/** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
|
|
11115
|
+
UNREACHABLE: "unreachable",
|
|
11116
|
+
/** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
|
|
11117
|
+
UNAUTHORIZED: "unauthorized",
|
|
11118
|
+
/** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
|
|
11119
|
+
TOOL_ERROR: "tool_error",
|
|
11120
|
+
/** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
|
|
11121
|
+
EMPTY: "empty"
|
|
11122
|
+
};
|
|
11123
|
+
var TOOL_FAILURE_TEXT = {
|
|
11124
|
+
[TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
|
|
11125
|
+
[TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
|
|
11126
|
+
[TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
|
|
11127
|
+
[TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
|
|
11128
|
+
[TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
|
|
11129
|
+
[TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
|
|
11130
|
+
[TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
|
|
11131
|
+
};
|
|
11132
|
+
var TOOL_FAILURE_RETRIABLE = {
|
|
11133
|
+
[TOOL_FAILURE.NOT_FOUND]: false,
|
|
11134
|
+
[TOOL_FAILURE.FORBIDDEN]: false,
|
|
11135
|
+
[TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
|
|
11136
|
+
[TOOL_FAILURE.UNREACHABLE]: true,
|
|
11137
|
+
// Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
|
|
11138
|
+
[TOOL_FAILURE.UNAUTHORIZED]: false,
|
|
11139
|
+
[TOOL_FAILURE.TOOL_ERROR]: false,
|
|
11140
|
+
[TOOL_FAILURE.EMPTY]: false
|
|
11141
|
+
};
|
|
11142
|
+
|
|
11083
11143
|
// src/workspace.ts
|
|
11084
11144
|
init_config();
|
|
11085
11145
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4 } from "fs";
|
package/dist/overmind.js
CHANGED
|
@@ -2275,6 +2275,7 @@ __export(config_exports, {
|
|
|
2275
2275
|
SWARM_VERIFY: () => SWARM_VERIFY,
|
|
2276
2276
|
SWARM_VERSION_CHECK_MS: () => SWARM_VERSION_CHECK_MS,
|
|
2277
2277
|
SWARM_WATCHDOG_STALL_MS: () => SWARM_WATCHDOG_STALL_MS,
|
|
2278
|
+
SWARM_WORKTREE_REAP_INTERVAL_MS: () => SWARM_WORKTREE_REAP_INTERVAL_MS,
|
|
2278
2279
|
TASKLESS_API: () => TASKLESS_API,
|
|
2279
2280
|
TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
|
|
2280
2281
|
TASKLESS_TASK_URL_BASE: () => TASKLESS_TASK_URL_BASE,
|
|
@@ -2371,7 +2372,7 @@ function reviewCutoffAt() {
|
|
|
2371
2372
|
}
|
|
2372
2373
|
return at;
|
|
2373
2374
|
}
|
|
2374
|
-
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, 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, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
|
|
2375
|
+
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, SWARM_WORKTREE_REAP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, 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, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
|
|
2375
2376
|
var init_config = __esm({
|
|
2376
2377
|
"src/config.ts"() {
|
|
2377
2378
|
"use strict";
|
|
@@ -2420,6 +2421,7 @@ var init_config = __esm({
|
|
|
2420
2421
|
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
2421
2422
|
SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
|
|
2422
2423
|
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
2424
|
+
SWARM_WORKTREE_REAP_INTERVAL_MS = envNum("SWARM_WORKTREE_REAP_INTERVAL_MS", 5 * 6e4);
|
|
2423
2425
|
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
2424
2426
|
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
2425
2427
|
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
@@ -3486,12 +3488,18 @@ async function previousHooksPath(worktree) {
|
|
|
3486
3488
|
}
|
|
3487
3489
|
async function removeWorktree(repo, path) {
|
|
3488
3490
|
const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
3491
|
+
await releaseWorktree(repo, path);
|
|
3492
|
+
if (branch) await $2({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
3493
|
+
}
|
|
3494
|
+
async function releaseWorktree(repo, path) {
|
|
3489
3495
|
await cleanupWorktreeProcesses(path);
|
|
3490
3496
|
await detachWorktree(repo, path);
|
|
3491
|
-
|
|
3497
|
+
}
|
|
3498
|
+
async function pruneWorktrees(repo) {
|
|
3499
|
+
await $2({ reject: false })`git -C ${repo} worktree prune`;
|
|
3492
3500
|
}
|
|
3493
3501
|
async function worktreeDirty(path) {
|
|
3494
|
-
const st = await $2({ reject: false })`git -C ${path} status --porcelain`;
|
|
3502
|
+
const st = await $2({ reject: false })`git -C ${path} status --porcelain --untracked-files=all`;
|
|
3495
3503
|
if (st.exitCode !== 0) return true;
|
|
3496
3504
|
const status = st.stdout.trim();
|
|
3497
3505
|
if (status) return true;
|
|
@@ -4519,6 +4527,78 @@ var init_oauth = __esm({
|
|
|
4519
4527
|
}
|
|
4520
4528
|
});
|
|
4521
4529
|
|
|
4530
|
+
// src/toolFailure.ts
|
|
4531
|
+
function trimDetail(s) {
|
|
4532
|
+
const t = s.trim();
|
|
4533
|
+
return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
|
|
4534
|
+
}
|
|
4535
|
+
function classifyToolFailure(raw) {
|
|
4536
|
+
if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
|
|
4537
|
+
if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
|
|
4538
|
+
if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
|
|
4539
|
+
return TOOL_FAILURE.TOOL_ERROR;
|
|
4540
|
+
}
|
|
4541
|
+
function classifyThrown(e) {
|
|
4542
|
+
const { kind, detail } = explainLinkFailure(e);
|
|
4543
|
+
const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
|
|
4544
|
+
// «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
|
|
4545
|
+
// и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
|
|
4546
|
+
classifyToolFailure(detail)
|
|
4547
|
+
);
|
|
4548
|
+
return { code, detail };
|
|
4549
|
+
}
|
|
4550
|
+
function describeToolFailure(f) {
|
|
4551
|
+
const text = TOOL_FAILURE_TEXT[f.code];
|
|
4552
|
+
const detail = trimDetail(f.detail);
|
|
4553
|
+
return detail ? `${text} (${detail})` : text;
|
|
4554
|
+
}
|
|
4555
|
+
var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
|
|
4556
|
+
var init_toolFailure = __esm({
|
|
4557
|
+
"src/toolFailure.ts"() {
|
|
4558
|
+
"use strict";
|
|
4559
|
+
init_linkFailure();
|
|
4560
|
+
TOOL_FAILURE = {
|
|
4561
|
+
/** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
|
|
4562
|
+
NOT_FOUND: "not_found",
|
|
4563
|
+
/** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
|
|
4564
|
+
FORBIDDEN: "forbidden",
|
|
4565
|
+
/** Токен роя не состоит в воркспейсе, которым искали. */
|
|
4566
|
+
WORKSPACE_MISMATCH: "workspace_mismatch",
|
|
4567
|
+
/** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
|
|
4568
|
+
UNREACHABLE: "unreachable",
|
|
4569
|
+
/** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
|
|
4570
|
+
UNAUTHORIZED: "unauthorized",
|
|
4571
|
+
/** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
|
|
4572
|
+
TOOL_ERROR: "tool_error",
|
|
4573
|
+
/** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
|
|
4574
|
+
EMPTY: "empty"
|
|
4575
|
+
};
|
|
4576
|
+
TOOL_FAILURE_TEXT = {
|
|
4577
|
+
[TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
|
|
4578
|
+
[TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
|
|
4579
|
+
[TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
|
|
4580
|
+
[TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
|
|
4581
|
+
[TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
|
|
4582
|
+
[TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
|
|
4583
|
+
[TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
|
|
4584
|
+
};
|
|
4585
|
+
TOOL_FAILURE_RETRIABLE = {
|
|
4586
|
+
[TOOL_FAILURE.NOT_FOUND]: false,
|
|
4587
|
+
[TOOL_FAILURE.FORBIDDEN]: false,
|
|
4588
|
+
[TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
|
|
4589
|
+
[TOOL_FAILURE.UNREACHABLE]: true,
|
|
4590
|
+
// Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
|
|
4591
|
+
[TOOL_FAILURE.UNAUTHORIZED]: false,
|
|
4592
|
+
[TOOL_FAILURE.TOOL_ERROR]: false,
|
|
4593
|
+
[TOOL_FAILURE.EMPTY]: false
|
|
4594
|
+
};
|
|
4595
|
+
FAILURE_DETAIL_MAX = 300;
|
|
4596
|
+
NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
|
|
4597
|
+
FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
|
|
4598
|
+
MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
|
|
4599
|
+
}
|
|
4600
|
+
});
|
|
4601
|
+
|
|
4522
4602
|
// src/authState.ts
|
|
4523
4603
|
function noteTokenRejected(nowMs = Date.now()) {
|
|
4524
4604
|
if (!denial) {
|
|
@@ -4615,25 +4695,44 @@ function isClosingTaskCategory(category) {
|
|
|
4615
4695
|
function taskAuthor(u) {
|
|
4616
4696
|
return u?.id ? { id: u.id, name: u.displayName || "\u0430\u0432\u0442\u043E\u0440 \u0437\u0430\u0434\u0430\u0447\u0438" } : null;
|
|
4617
4697
|
}
|
|
4698
|
+
function toolErrorText(r) {
|
|
4699
|
+
const fromContent = (r.content ?? []).map((c) => c.text ?? "").filter(Boolean).join(" ").trim();
|
|
4700
|
+
return fromContent || JSON.stringify(r);
|
|
4701
|
+
}
|
|
4618
4702
|
function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl = fetch) {
|
|
4619
4703
|
const ws = workspaceId ? { workspaceId } : {};
|
|
4620
4704
|
const idCache = /* @__PURE__ */ new Map();
|
|
4621
4705
|
const byWorkspace = /* @__PURE__ */ new Map();
|
|
4622
|
-
async function
|
|
4706
|
+
async function callTool(name, args) {
|
|
4623
4707
|
try {
|
|
4624
4708
|
const r = await call(name, WS_FREE_TOOLS.has(name) ? args : { ...args, ...ws });
|
|
4625
4709
|
if (r?.isError) {
|
|
4626
|
-
|
|
4627
|
-
|
|
4710
|
+
const raw = toolErrorText(r);
|
|
4711
|
+
const detail = trimDetail(raw);
|
|
4712
|
+
console.error(`[taskless] \u0442\u0443\u043B ${name} \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443:`, detail);
|
|
4713
|
+
return { ok: false, failure: { code: classifyToolFailure(raw), detail } };
|
|
4628
4714
|
}
|
|
4629
|
-
return r;
|
|
4715
|
+
return { ok: true, res: r };
|
|
4630
4716
|
} catch (e) {
|
|
4631
|
-
|
|
4632
|
-
|
|
4717
|
+
const failure = classifyThrown(e);
|
|
4718
|
+
console.error(`[taskless] \u0442\u0443\u043B ${name} \u0443\u043F\u0430\u043B:`, failure.detail);
|
|
4719
|
+
return { ok: false, failure };
|
|
4633
4720
|
}
|
|
4634
4721
|
}
|
|
4635
|
-
async function
|
|
4636
|
-
const
|
|
4722
|
+
async function safeCall(name, args) {
|
|
4723
|
+
const out = await callTool(name, args);
|
|
4724
|
+
return out.ok ? out.res : null;
|
|
4725
|
+
}
|
|
4726
|
+
async function getTaskDetailed(idOrSlug) {
|
|
4727
|
+
const out = await callTool("get_task", { id: idOrSlug });
|
|
4728
|
+
if (!out.ok) return { ok: false, failure: out.failure };
|
|
4729
|
+
const body = taskBodyFrom(out.res, idOrSlug);
|
|
4730
|
+
if (!body) {
|
|
4731
|
+
return { ok: false, failure: { code: TOOL_FAILURE.EMPTY, detail: "get_task: task \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435" } };
|
|
4732
|
+
}
|
|
4733
|
+
return { ok: true, task: body };
|
|
4734
|
+
}
|
|
4735
|
+
function taskBodyFrom(res, idOrSlug) {
|
|
4637
4736
|
const task = res?.structuredContent?.task ?? null;
|
|
4638
4737
|
if (!task?.id) return null;
|
|
4639
4738
|
const slug = task.prefix && task.number != null ? `${task.prefix}-${task.number}` : idOrSlug;
|
|
@@ -4663,6 +4762,9 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
4663
4762
|
idCache.set(body.id, body.id);
|
|
4664
4763
|
return body;
|
|
4665
4764
|
}
|
|
4765
|
+
async function getTask(idOrSlug) {
|
|
4766
|
+
return taskBodyFrom(await safeCall("get_task", { id: idOrSlug }), idOrSlug);
|
|
4767
|
+
}
|
|
4666
4768
|
async function getDocument(id) {
|
|
4667
4769
|
const res = await safeCall("get_document", { id });
|
|
4668
4770
|
const doc = res?.structuredContent?.document ?? null;
|
|
@@ -4754,6 +4856,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
4754
4856
|
}
|
|
4755
4857
|
const self = {
|
|
4756
4858
|
getTask,
|
|
4859
|
+
getTaskDetailed,
|
|
4757
4860
|
...fetchFile ? { fetchFile } : {},
|
|
4758
4861
|
startTask: (idOrSlug) => transition("start_task", idOrSlug),
|
|
4759
4862
|
// TASWM-294: intent — «человек, посмотри», а не «Готово» (см. докблок в TasklessClient).
|
|
@@ -4849,6 +4952,10 @@ function parseWorkspaces(res) {
|
|
|
4849
4952
|
return arr.filter((w) => !!w && typeof w.id === "string" && typeof w.slug === "string").map((w) => ({ id: w.id, slug: w.slug, name: typeof w.name === "string" ? w.name : w.slug, role: w.role }));
|
|
4850
4953
|
}
|
|
4851
4954
|
function requireTaskBody(task, slug, workspaceId) {
|
|
4955
|
+
if (task && "ok" in task) {
|
|
4956
|
+
if (task.ok) return task.task;
|
|
4957
|
+
throw new TaskBodyUnavailableError(slug, workspaceId, task.failure);
|
|
4958
|
+
}
|
|
4852
4959
|
if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
|
|
4853
4960
|
return task;
|
|
4854
4961
|
}
|
|
@@ -5080,16 +5187,25 @@ var init_taskless = __esm({
|
|
|
5080
5187
|
init_oauth();
|
|
5081
5188
|
init_agentToken();
|
|
5082
5189
|
init_linkFailure();
|
|
5190
|
+
init_toolFailure();
|
|
5083
5191
|
init_authState();
|
|
5084
5192
|
init_workspace();
|
|
5085
5193
|
SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
5086
5194
|
WS_FREE_TOOLS = /* @__PURE__ */ new Set(["list_workspaces", "who_am_i", "finalize_upload", "report_acceptance_check"]);
|
|
5087
5195
|
TaskBodyUnavailableError = class extends Error {
|
|
5088
|
-
|
|
5196
|
+
/** Код причины — по нему решают, повторять ли (TOOL_FAILURE_RETRIABLE), а не по тексту. */
|
|
5197
|
+
code;
|
|
5198
|
+
// TASWM-484: причина называется, а не перечисляется. Прежний текст («задачи там нет
|
|
5199
|
+
// ЛИБО токену её не видно») заставлял человека проверять обе версии руками — 21.08 на
|
|
5200
|
+
// этом ушло полдня, и обе оказались не теми. Теперь код приходит от классификатора
|
|
5201
|
+
// ответа прода, а человеку достаётся объяснение и то, что прод сказал дословно.
|
|
5202
|
+
constructor(slug, workspaceId, failure) {
|
|
5203
|
+
const f = failure ?? { code: TOOL_FAILURE.EMPTY, detail: "" };
|
|
5089
5204
|
super(
|
|
5090
|
-
`\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F: ${slug} \u0432 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 ${workspaceId || "(\u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0438\u043D \u0434\u0435\u043C\u043E\u043D\u0430)"} \u2014
|
|
5205
|
+
`\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F: ${slug} \u0432 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 ${workspaceId || "(\u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0438\u043D \u0434\u0435\u043C\u043E\u043D\u0430)"} \u2014 ${describeToolFailure(f)}. \u0414\u0440\u043E\u043D \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u0442: \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u0441\u043B\u0430\u0433\u0443, \u0431\u0435\u0437 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u0432, \u0434\u043E\u0440\u043E\u0436\u0435 \u0438 \u0432\u0440\u0435\u0434\u043D\u0435\u0435, \u0447\u0435\u043C \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0442\u044C (TASWM-288).`
|
|
5091
5206
|
);
|
|
5092
5207
|
this.name = "TaskBodyUnavailableError";
|
|
5208
|
+
this.code = f.code;
|
|
5093
5209
|
}
|
|
5094
5210
|
};
|
|
5095
5211
|
TASK_CONTEXT_MAX_DOCS = 5;
|
|
@@ -10507,6 +10623,70 @@ function parkReason(attempts, reason) {
|
|
|
10507
10623
|
return `${PRE_START_MARK} ${attempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u043E \u043A\u0440\u0443\u0433\u0443. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
|
|
10508
10624
|
}
|
|
10509
10625
|
|
|
10626
|
+
// src/worktreeReap.ts
|
|
10627
|
+
init_src();
|
|
10628
|
+
var DIRTY_WORKTREE_MARK = "worktree \u043D\u0435 \u0443\u0431\u0440\u0430\u043D: \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430";
|
|
10629
|
+
var HOLDS_WORKTREE = /* @__PURE__ */ new Set(["running", "blocked", "review", "paused", "storm_dead", "stalled"]);
|
|
10630
|
+
function isHelperRun(run) {
|
|
10631
|
+
return run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null;
|
|
10632
|
+
}
|
|
10633
|
+
function prIsDead(st) {
|
|
10634
|
+
return st.merged || st.state.toUpperCase() === "MERGED" || st.state.toUpperCase() === "CLOSED";
|
|
10635
|
+
}
|
|
10636
|
+
function heldByLive(deps, all, run, path) {
|
|
10637
|
+
if (deps.liveWorktrees?.().has(path)) return true;
|
|
10638
|
+
return all.some((r) => r.id !== run.id && r.worktree_path === path && HOLDS_WORKTREE.has(r.status));
|
|
10639
|
+
}
|
|
10640
|
+
async function reapFinishedWorktrees(deps) {
|
|
10641
|
+
const report = { removed: 0, keptDirty: 0 };
|
|
10642
|
+
const prunedRepos = /* @__PURE__ */ new Set();
|
|
10643
|
+
const all = deps.db.listRuns();
|
|
10644
|
+
for (const run of all) {
|
|
10645
|
+
const path = run.worktree_path;
|
|
10646
|
+
if (!path || !isTerminalStatus(run.status)) continue;
|
|
10647
|
+
if (heldByLive(deps, all, run, path)) continue;
|
|
10648
|
+
if (!deps.worktreeExists(path)) {
|
|
10649
|
+
if (run.repo && deps.pruneWorktrees && !prunedRepos.has(run.repo)) {
|
|
10650
|
+
prunedRepos.add(run.repo);
|
|
10651
|
+
await deps.pruneWorktrees(run.repo).catch(() => {
|
|
10652
|
+
});
|
|
10653
|
+
}
|
|
10654
|
+
continue;
|
|
10655
|
+
}
|
|
10656
|
+
if (!isHelperRun(run)) {
|
|
10657
|
+
if (!run.pr_url) continue;
|
|
10658
|
+
const st = await deps.prStatus(run.pr_url).catch(() => null);
|
|
10659
|
+
if (!st || !prIsDead(st)) continue;
|
|
10660
|
+
}
|
|
10661
|
+
if (await keepDirty(deps, run, path)) {
|
|
10662
|
+
report.keptDirty += 1;
|
|
10663
|
+
continue;
|
|
10664
|
+
}
|
|
10665
|
+
await deps.releaseWorktree(run.repo, path).catch(() => {
|
|
10666
|
+
});
|
|
10667
|
+
deps.db.addEvent(run.id, "step", `worktree \u0443\u0431\u0440\u0430\u043D: ${path} (\u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D, ${reapReason(run)}) \u2014 \u0441\u043B\u0430\u0433 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430`);
|
|
10668
|
+
report.removed += 1;
|
|
10669
|
+
}
|
|
10670
|
+
return report;
|
|
10671
|
+
}
|
|
10672
|
+
function reapReason(run) {
|
|
10673
|
+
return isHelperRun(run) ? "\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0441\u043A\u0430\u044F \u043A\u043E\u043F\u0438\u044F \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u0430" : "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0438\u043B\u0438 \u0437\u0430\u043A\u0440\u044B\u0442";
|
|
10674
|
+
}
|
|
10675
|
+
async function keepDirty(deps, run, path) {
|
|
10676
|
+
const dirty = deps.isDirty ? await deps.isDirty(path).catch(() => true) : true;
|
|
10677
|
+
if (!dirty) return false;
|
|
10678
|
+
const already = deps.db.events(run.id).some((e) => (e.text ?? "").includes(DIRTY_WORKTREE_MARK));
|
|
10679
|
+
if (!already) {
|
|
10680
|
+
deps.db.addEvent(
|
|
10681
|
+
run.id,
|
|
10682
|
+
"anomaly",
|
|
10683
|
+
`${DIRTY_WORKTREE_MARK}. \u041F\u0443\u0442\u044C: ${path}. \u0412\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u0438\u043B\u0438 \u043D\u0435\u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u043B\u0438\u0431\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u044B, \u043D\u0435 \u0434\u043E\u0448\u0435\u0434\u0448\u0438\u0435 \u0434\u043E origin, \u2014 \u0441\u043D\u043E\u0441\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F, \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C \u0438 \u0443\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`).`
|
|
10684
|
+
);
|
|
10685
|
+
deps.narrate?.(`${run.task_slug}: worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u2014 \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (${path}).`);
|
|
10686
|
+
}
|
|
10687
|
+
return true;
|
|
10688
|
+
}
|
|
10689
|
+
|
|
10510
10690
|
// src/overmind.ts
|
|
10511
10691
|
init_git();
|
|
10512
10692
|
|
|
@@ -10696,10 +10876,24 @@ function fullCommitHeadline(headline, body) {
|
|
|
10696
10876
|
return head.slice(0, -1) + tail2;
|
|
10697
10877
|
}
|
|
10698
10878
|
var NO_CHECKS_STDERR = /no checks reported/i;
|
|
10879
|
+
var CiAccessDeniedError = class extends Error {
|
|
10880
|
+
detail;
|
|
10881
|
+
constructor(detail) {
|
|
10882
|
+
super(`\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C: ${detail}`);
|
|
10883
|
+
this.name = "CiAccessDeniedError";
|
|
10884
|
+
this.detail = detail;
|
|
10885
|
+
}
|
|
10886
|
+
};
|
|
10887
|
+
var DENIED_STDERR = /(?:^|\D)403(?:\D|$)|not accessible by integration|must have admin rights|resource not accessible/i;
|
|
10888
|
+
function firstLine(s) {
|
|
10889
|
+
return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "gh \u0432\u0435\u0440\u043D\u0443\u043B 403";
|
|
10890
|
+
}
|
|
10699
10891
|
var realChecksRunner = async (pr) => {
|
|
10700
10892
|
const r = await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
|
|
10701
10893
|
const stdout = r.stdout ?? "";
|
|
10702
|
-
|
|
10894
|
+
const stderr = r.stderr ?? "";
|
|
10895
|
+
if (!stdout.trim() && NO_CHECKS_STDERR.test(stderr)) return "[]";
|
|
10896
|
+
if (!stdout.trim() && DENIED_STDERR.test(stderr)) throw new CiAccessDeniedError(firstLine(stderr));
|
|
10703
10897
|
return stdout;
|
|
10704
10898
|
};
|
|
10705
10899
|
async function prCheckRows(pr, run) {
|
|
@@ -10708,13 +10902,17 @@ async function prCheckRows(pr, run) {
|
|
|
10708
10902
|
const rows = JSON.parse(out);
|
|
10709
10903
|
if (!Array.isArray(rows)) return null;
|
|
10710
10904
|
return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
|
|
10711
|
-
} catch {
|
|
10905
|
+
} catch (e) {
|
|
10906
|
+
if (e instanceof CiAccessDeniedError) return { denied: e.detail };
|
|
10712
10907
|
return null;
|
|
10713
10908
|
}
|
|
10714
10909
|
}
|
|
10910
|
+
function isDenied(rows) {
|
|
10911
|
+
return rows != null && !Array.isArray(rows);
|
|
10912
|
+
}
|
|
10715
10913
|
async function ghChecks(pr, run = realChecksRunner) {
|
|
10716
10914
|
const rows = await prCheckRows(pr, run);
|
|
10717
|
-
if (!rows || rows.length === 0) return null;
|
|
10915
|
+
if (!rows || isDenied(rows) || rows.length === 0) return null;
|
|
10718
10916
|
let pass = 0, fail = 0, pending2 = 0;
|
|
10719
10917
|
for (const r of rows) {
|
|
10720
10918
|
if (r.bucket === "pass") pass++;
|
|
@@ -10745,6 +10943,7 @@ function runIdFromLink(link) {
|
|
|
10745
10943
|
async function prCiStatus(pr, run = realChecksRunner) {
|
|
10746
10944
|
const rows = await prCheckRows(pr, run);
|
|
10747
10945
|
if (!rows) return null;
|
|
10946
|
+
if (isDenied(rows)) return { bucket: "denied", failing: [], failingRunIds: [], total: 0, pass: 0, pending: 0, detail: rows.denied };
|
|
10748
10947
|
if (rows.length === 0) return { bucket: "none", failing: [], failingRunIds: [], total: 0, pass: 0, pending: 0 };
|
|
10749
10948
|
const total = rows.length;
|
|
10750
10949
|
const pass = rows.filter((r) => r.bucket === "pass").length;
|
|
@@ -11740,6 +11939,52 @@ import { createHash as createHash3 } from "crypto";
|
|
|
11740
11939
|
|
|
11741
11940
|
// src/anomalies.ts
|
|
11742
11941
|
init_src();
|
|
11942
|
+
|
|
11943
|
+
// src/reviewRescue.ts
|
|
11944
|
+
init_src();
|
|
11945
|
+
var REVIEW_RESCUE_AFTER_MS = Math.max(6e4, Number(process.env.SWARM_REVIEW_RESCUE_MS) || 10 * 6e4);
|
|
11946
|
+
var REVIEW_RESCUE_MARK = "\u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E";
|
|
11947
|
+
var AUTHOR_GONE_STATUSES = /* @__PURE__ */ new Set(["paused", "failed", "stalled"]);
|
|
11948
|
+
function isExecutorRun(run) {
|
|
11949
|
+
if (run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null) return false;
|
|
11950
|
+
const kind = run.kind ?? "drone";
|
|
11951
|
+
return kind === "drone" || kind === "hero";
|
|
11952
|
+
}
|
|
11953
|
+
function prOpenedAtMs(events) {
|
|
11954
|
+
const opened = events.filter((e) => e.kind === "pr_opened");
|
|
11955
|
+
if (opened.length === 0) return null;
|
|
11956
|
+
const ms = sqliteMs(opened[opened.length - 1].at);
|
|
11957
|
+
return Number.isFinite(ms) ? ms : null;
|
|
11958
|
+
}
|
|
11959
|
+
function reviewlessPrCandidates(db, now, afterMs = REVIEW_RESCUE_AFTER_MS) {
|
|
11960
|
+
const out = [];
|
|
11961
|
+
for (const run of db.listRuns()) {
|
|
11962
|
+
if (!run.pr_url) continue;
|
|
11963
|
+
if (run.status !== "review" && !AUTHOR_GONE_STATUSES.has(run.status)) continue;
|
|
11964
|
+
if (!isExecutorRun(run)) continue;
|
|
11965
|
+
if (run.review_verdict != null) continue;
|
|
11966
|
+
if (run.status === "review") {
|
|
11967
|
+
if (db.reviewerCountFor(run.id) > 0) continue;
|
|
11968
|
+
if (db.activeReviewersFor(run.id).length > 0) continue;
|
|
11969
|
+
}
|
|
11970
|
+
const events = db.events(run.id);
|
|
11971
|
+
const openedAt = prOpenedAtMs(events) ?? sqliteMs(run.updated_at);
|
|
11972
|
+
if (!Number.isFinite(openedAt) || now - openedAt < afterMs) continue;
|
|
11973
|
+
out.push(run);
|
|
11974
|
+
}
|
|
11975
|
+
return out;
|
|
11976
|
+
}
|
|
11977
|
+
function lastReviewBlockReason(db, runId) {
|
|
11978
|
+
const hit = db.events(runId).findLast((e) => (e.text ?? "").includes(`${REVIEW_RESCUE_MARK}:`));
|
|
11979
|
+
if (!hit?.text) return null;
|
|
11980
|
+
const idx = hit.text.indexOf(`${REVIEW_RESCUE_MARK}:`);
|
|
11981
|
+
return hit.text.slice(idx + REVIEW_RESCUE_MARK.length + 1).trim() || null;
|
|
11982
|
+
}
|
|
11983
|
+
function shouldNoteReason(db, runId, reason) {
|
|
11984
|
+
return lastReviewBlockReason(db, runId) !== reason;
|
|
11985
|
+
}
|
|
11986
|
+
|
|
11987
|
+
// src/anomalies.ts
|
|
11743
11988
|
var LEVEL_RANK = { note: 0, feed: 1, decision: 2 };
|
|
11744
11989
|
function plural2(n, forms) {
|
|
11745
11990
|
const mod100 = n % 100;
|
|
@@ -12107,15 +12352,18 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
12107
12352
|
key: `pr_no_reviewer:${run.id}`,
|
|
12108
12353
|
minDurationMs: thresholds.prNoReviewerMs,
|
|
12109
12354
|
group: PR_GROUP,
|
|
12110
|
-
build: () =>
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12355
|
+
build: () => {
|
|
12356
|
+
const why = lastReviewBlockReason(db, run.id);
|
|
12357
|
+
return {
|
|
12358
|
+
runId: run.id,
|
|
12359
|
+
// В ленту: рой действует сам (автоподъём), решения человека тут не требуется —
|
|
12360
|
+
// если и подъём не вышел, причина в теле и есть то, с чем человек идёт разбираться.
|
|
12361
|
+
level: "feed",
|
|
12362
|
+
title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
|
|
12363
|
+
body: `${droneOf(run)}: PR \u043E\u0442\u043A\u0440\u044B\u0442 \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0442\u0430\u043A \u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u2014 ${why ?? "\u0440\u043E\u0439 \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B \u043F\u0440\u0438\u0447\u0438\u043D\u0443 (\u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0434\u043E\u0448\u0451\u043B \u0434\u043E \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430)"} \u2014 ${run.pr_url}.`,
|
|
12364
|
+
...hrefOf(run.task_url)
|
|
12365
|
+
};
|
|
12366
|
+
}
|
|
12119
12367
|
});
|
|
12120
12368
|
} else if (run.review_verdict === "changes" && db.activeReviewersFor(run.id).length === 0) {
|
|
12121
12369
|
candidates.push({
|
|
@@ -13191,7 +13439,7 @@ async function changedPaths(worktree, opts = {}) {
|
|
|
13191
13439
|
// package.json
|
|
13192
13440
|
var package_default = {
|
|
13193
13441
|
name: "@taskless-app/swarm",
|
|
13194
|
-
version: "0.6.
|
|
13442
|
+
version: "0.6.21",
|
|
13195
13443
|
type: "module",
|
|
13196
13444
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
13197
13445
|
keywords: [
|
|
@@ -13241,7 +13489,7 @@ var package_default = {
|
|
|
13241
13489
|
execa: "^10.0.0",
|
|
13242
13490
|
zod: "3.25.76"
|
|
13243
13491
|
},
|
|
13244
|
-
gitHead: "
|
|
13492
|
+
gitHead: "584411cbe69583a8727e89caa8ae7e626f8fd8ca"
|
|
13245
13493
|
};
|
|
13246
13494
|
|
|
13247
13495
|
// src/version.ts
|
|
@@ -15745,6 +15993,9 @@ var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u04
|
|
|
15745
15993
|
var CI_NO_CHECKS_MARK = "\u0443 PR \u043D\u0435\u0442 \u0447\u0435\u043A\u043E\u0432 \u0438 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442";
|
|
15746
15994
|
var CI_WAIT_POLL_MARK = "\u0436\u0434\u0443 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A";
|
|
15747
15995
|
var CI_POLL_LIMIT_MARK = "\u043E\u043F\u0440\u043E\u0441 CI \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u043E \u043F\u043E\u0442\u043E\u043B\u043A\u0443";
|
|
15996
|
+
var CI_DENIED_LIMIT = Number(process.env.SWARM_CI_DENIED_LIMIT) || 3;
|
|
15997
|
+
var CI_DENIED_MARK = "\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C (403)";
|
|
15998
|
+
var CI_DENIED_STOP_MARK = "\u043E\u043F\u0440\u043E\u0441 CI \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 (403)";
|
|
15748
15999
|
var CI_WAIT_LIMIT_MS = Number(process.env.SWARM_CI_WAIT_LIMIT_MS) || 45 * 60 * 1e3;
|
|
15749
16000
|
var CI_POLL_LIMIT = Math.max(
|
|
15750
16001
|
Number(process.env.SWARM_CI_POLL_LIMIT) || 90,
|
|
@@ -16091,6 +16342,92 @@ async function closeMergedTaskRuns(deps) {
|
|
|
16091
16342
|
}
|
|
16092
16343
|
}
|
|
16093
16344
|
}
|
|
16345
|
+
function relinkUnboundReviewers(deps) {
|
|
16346
|
+
const all = deps.db.listRuns();
|
|
16347
|
+
const unbound = all.filter((r) => r.kind === "reviewer" && r.reviews_run_id == null && r.pr_url && REVIEWER_ALIVE_STATUSES.has(r.status));
|
|
16348
|
+
if (unbound.length === 0) return;
|
|
16349
|
+
for (const rv of unbound) {
|
|
16350
|
+
const candidates = all.filter(
|
|
16351
|
+
(r) => r.id !== rv.id && r.pr_url === rv.pr_url && (r.kind ?? "drone") !== "reviewer" && (r.kind ?? "drone") !== "verifier" && r.reviews_run_id == null && r.verifies_run_id == null && r.resolves_run_id == null
|
|
16352
|
+
);
|
|
16353
|
+
if (candidates.length !== 1) {
|
|
16354
|
+
const why = candidates.length === 0 ? `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 ${rv.pr_url} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430` : `\u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 ${rv.pr_url} \u043D\u0430\u0439\u0434\u0435\u043D\u043E ${candidates.length} \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u2014 \u043A\u0430\u043A\u043E\u0439 \u0438\u0437 \u043D\u0438\u0445 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439, \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F`;
|
|
16355
|
+
if (!deps.db.events(rv.id).some((e) => (e.text ?? "").includes(why))) deps.db.addEvent(rv.id, "step", why);
|
|
16356
|
+
continue;
|
|
16357
|
+
}
|
|
16358
|
+
const parent = candidates[0];
|
|
16359
|
+
deps.db.updateRun(rv.id, { reviews_run_id: parent.id, review_cycle: parent.review_cycle });
|
|
16360
|
+
deps.db.addEvent(rv.id, "step", `\u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 PR: #${parent.id} (${parent.task_slug}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0435\u0441\u0442\u044C \u043A\u0443\u0434\u0430 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C (TASWM-375)`);
|
|
16361
|
+
deps.db.addEvent(parent.id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${rv.name} (#${rv.id}) \u043F\u043E\u0434\u043D\u044F\u0442 \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u0435\u0433\u043E \u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043B\u044F\u0436\u0435\u0442 \u0441\u044E\u0434\u0430`);
|
|
16362
|
+
narrate(deps, `${droneLabel(rv)} \u0440\u0430\u0431\u043E\u0442\u0430\u043B \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0443 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0442\u0435\u043F\u0435\u0440\u044C \u043B\u044F\u0436\u0435\u0442 \u0432 ${parent.task_slug}, \u0430 \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442 GitHub.`);
|
|
16363
|
+
}
|
|
16364
|
+
}
|
|
16365
|
+
function deliberateReviewStop(db, run) {
|
|
16366
|
+
const texts = db.events(run.id).map((e) => e.text ?? "");
|
|
16367
|
+
const has = (mark) => texts.some((t) => t.includes(mark));
|
|
16368
|
+
if (has(SELF_REFERENCE_MARK)) {
|
|
16369
|
+
return `PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 (${DAEMON_CODE_PATHS.join(", ")}) \u2014 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E (TASWM-270): \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0438\u0439 \u0434\u0435\u043C\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u043C \u043A\u043E\u0434\u0435 \u0438 \u0441\u0432\u043E\u044E \u0436\u0435 \u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0435 \u0430\u0442\u0442\u0435\u0441\u0442\u0443\u0435\u0442. \u041D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
|
|
16370
|
+
}
|
|
16371
|
+
if (has(REVIEW_ESCALATION_MARK)) return `\u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B (${REVIEW_SPAWN_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
|
|
16372
|
+
if (has(REVIEW_CYCLE_ESCALATION_MARK)) return `\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0446\u0438\u043A\u043B\u043E\u0432 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E (${REVIEW_CYCLE_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
|
|
16373
|
+
if (has(reviewBudgetMark(run.review_cycle ?? 0))) return "\u0440\u0435\u0432\u044C\u044E \u044D\u0442\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0441\u0433\u043E\u0440\u0435\u043B\u043E \u043D\u0430 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 \u0442\u0440\u0430\u0442 \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0442\u043E\u0442 \u0436\u0435, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
|
|
16374
|
+
if (has(reviewUnrecoverableMark(run.branch || run.task_slug))) return "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
|
|
16375
|
+
return null;
|
|
16376
|
+
}
|
|
16377
|
+
function noteReviewBlocked(deps, run, reason) {
|
|
16378
|
+
if (!shouldNoteReason(deps.db, run.id, reason)) return;
|
|
16379
|
+
deps.db.addEvent(run.id, "step", `${REVIEW_RESCUE_MARK}: ${reason}`);
|
|
16380
|
+
}
|
|
16381
|
+
async function rescueReviewlessPrs(deps, stormActive, now) {
|
|
16382
|
+
for (const run of reviewlessPrCandidates(deps.db, now)) {
|
|
16383
|
+
try {
|
|
16384
|
+
const stop = deliberateReviewStop(deps.db, run);
|
|
16385
|
+
if (stop) {
|
|
16386
|
+
noteReviewBlocked(deps, run, stop);
|
|
16387
|
+
continue;
|
|
16388
|
+
}
|
|
16389
|
+
const authorGone = AUTHOR_GONE_STATUSES.has(run.status) && (run.status !== "paused" || NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r)));
|
|
16390
|
+
if (authorGone) {
|
|
16391
|
+
const st = await deps.prStatus(run.pr_url).catch(() => null);
|
|
16392
|
+
const liveness = prLiveness(st);
|
|
16393
|
+
if (liveness !== "open") {
|
|
16394
|
+
noteReviewBlocked(deps, run, liveness === "unknown" ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043F\u0440\u043E PR \u2014 \u043D\u0435 \u0437\u043D\u0430\u044E, \u0436\u0438\u0432 \u043B\u0438 \u043E\u043D; \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" : `PR ${liveness === "merged" ? "\u0441\u043C\u0435\u0440\u0436\u0435\u043D" : "\u0437\u0430\u043A\u0440\u044B\u0442"} \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
|
|
16395
|
+
continue;
|
|
16396
|
+
}
|
|
16397
|
+
if (st.isDraft) {
|
|
16398
|
+
noteReviewBlocked(deps, run, "PR \u2014 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A: \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u043D\u0435 \u0441\u043C\u043E\u0442\u0440\u0438\u0442 (\u0441\u043D\u0438\u043C\u0438 draft)");
|
|
16399
|
+
continue;
|
|
16400
|
+
}
|
|
16401
|
+
const wasLabel = STATUS_META[run.status]?.label ?? run.status;
|
|
16402
|
+
deps.db.updateRun(run.id, { status: "review" });
|
|
16403
|
+
deps.db.addEvent(
|
|
16404
|
+
run.id,
|
|
16405
|
+
"step",
|
|
16406
|
+
`${REVIEW_RESCUE_MARK}: \u0430\u0432\u0442\u043E\u0440 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (${wasLabel}), \u0430 PR \u043E\u0442\u043A\u0440\u044B\u0442 \u0438 \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0438\u0439 \u043D\u0443\u0436\u0435\u043D \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0441\u0443\u0434\u044C\u0431\u044B \u0430\u0432\u0442\u043E\u0440\u0430`
|
|
16407
|
+
);
|
|
16408
|
+
narrate(deps, `PR ${droneLabel(run)} \u0436\u0438\u0432, \u0430 \u0435\u0433\u043E \u0430\u0432\u0442\u043E\u0440 \u2014 \u043D\u0435\u0442 (${wasLabel}): \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043D\u0443\u0436\u0435\u043D.`);
|
|
16409
|
+
continue;
|
|
16410
|
+
}
|
|
16411
|
+
if (run.status !== "review") continue;
|
|
16412
|
+
noteReviewBlocked(deps, run, reviewGateReason(deps, run, stormActive));
|
|
16413
|
+
} catch (e) {
|
|
16414
|
+
console.error(`[overmind] \u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
16415
|
+
}
|
|
16416
|
+
}
|
|
16417
|
+
}
|
|
16418
|
+
function reviewGateReason(deps, run, stormActive) {
|
|
16419
|
+
if (!deps.spawnReviewer) return "\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 (SWARM_AUTO_REVIEW=0)";
|
|
16420
|
+
if (stormActive) return "\u0448\u0442\u043E\u0440\u043C Anthropic \u2014 \u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u0440\u043E\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442, \u0440\u0435\u0432\u044C\u044E \u0436\u0434\u0451\u0442 \u043E\u0442\u0431\u043E\u044F";
|
|
16421
|
+
if (deps.reviewCutoffAt && run.updated_at < deps.reviewCutoffAt) {
|
|
16422
|
+
return `\u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u0430\u0440\u0448\u0435 \u043C\u043E\u043C\u0435\u043D\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E (cutoff ${deps.reviewCutoffAt}) \u2014 \u0440\u043E\u0439 \u0435\u0433\u043E \u043D\u0435 \u0431\u0435\u0440\u0451\u0442, \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0440\u0443\u043A\u0430\u043C\u0438`;
|
|
16423
|
+
}
|
|
16424
|
+
if (deps.resourcesOk && !deps.resourcesOk()) return "\u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0436\u0434\u0451\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0445 \u0440\u0435\u0441\u0443\u0440\u0441\u043E\u0432";
|
|
16425
|
+
if (deps.reviewCap !== void 0) {
|
|
16426
|
+
const busy = countActiveReviewers(deps.db);
|
|
16427
|
+
if (busy >= deps.reviewCap) return `\u0437\u0430\u043D\u044F\u0442 \u0441\u043B\u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 ${busy} \u043F\u0440\u0438 SWARM_REVIEW_CAP=${deps.reviewCap} \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u043B\u043E\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F`;
|
|
16428
|
+
}
|
|
16429
|
+
return "\u0432\u0441\u0435 \u0433\u0435\u0439\u0442\u044B \u043E\u0442\u043A\u0440\u044B\u0442\u044B, \u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u2014 \u0441\u043C\u043E\u0442\u0440\u0438 \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0430 \u043E\u0448\u0438\u0431\u043A\u0443 \u0441\u043F\u0430\u0432\u043D\u0430";
|
|
16430
|
+
}
|
|
16094
16431
|
async function closeOrphanedReviewers(deps) {
|
|
16095
16432
|
for (const rv of deps.db.listRuns()) {
|
|
16096
16433
|
const checkedId = rv.kind === "reviewer" ? rv.reviews_run_id : rv.kind === "verifier" ? rv.verifies_run_id : null;
|
|
@@ -16156,8 +16493,13 @@ function buildCiWakeNudge(summary) {
|
|
|
16156
16493
|
].join("\n");
|
|
16157
16494
|
}
|
|
16158
16495
|
function buildCiReadyNudge(prUrl, over) {
|
|
16159
|
-
const counts = over.kind
|
|
16160
|
-
const head = over.kind === "none" ? `\u2139\uFE0F \u0416\u0434\u0430\u0442\u044C CI \u043D\u0430 PR ${prUrl} \u041D\u0415\u0427\u0415\u0413\u041E: \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 \u0443 PR \u043D\u0435\u0442, \u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E workflow \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0442\u0443\u0442 \u043D\u0435 \u0431\u044B\u0432\u0430\u0435\u0442. \u0420\u043E\u0439 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D), \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u044D\u0442\u043E \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.` : over.kind === "gaveup" ? `\u26A0\uFE0F \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0442\u0430\u043A \u0438 \u041D\u0415 \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u041B\u0418\u0421\u042C \u2014 CI \u043E\u0442\u0432\u0430\u043B\u0438\u043B\u0441\u044F \u0438\u043B\u0438 \u043D\u0430\u043C\u0435\u0440\u0442\u0432\u043E \u0432\u0438\u0441\u0438\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u0438 \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u0435\u0433\u043E \u043E\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044C (\u0441\u0440\u043E\u043A\u0438 \u0438 \u0441\u0447\u0451\u0442\u0447\u0438\u043A \u2014 \u0432 \u0441\u043E\u0431\u044B\u0442\u0438\u044F\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430). \u0420\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0432\u043E\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D). \u041D\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0439 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0447\u0438\u043D\u0438\u0442\u044C \u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u043F\u0440\u043E \u044D\u0442\u043E \u0440\u043E\u0439 \u0443\u0436\u0435 \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C.` :
|
|
16496
|
+
const counts = (over.kind === "pass" || over.kind === "none") && over.ci.total > 0 ? ` (${over.ci.pass}/${over.ci.total})` : "";
|
|
16497
|
+
const head = over.kind === "none" ? `\u2139\uFE0F \u0416\u0434\u0430\u0442\u044C CI \u043D\u0430 PR ${prUrl} \u041D\u0415\u0427\u0415\u0413\u041E: \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 \u0443 PR \u043D\u0435\u0442, \u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E workflow \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0442\u0443\u0442 \u043D\u0435 \u0431\u044B\u0432\u0430\u0435\u0442. \u0420\u043E\u0439 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D), \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u044D\u0442\u043E \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.` : over.kind === "gaveup" ? `\u26A0\uFE0F \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0442\u0430\u043A \u0438 \u041D\u0415 \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u041B\u0418\u0421\u042C \u2014 CI \u043E\u0442\u0432\u0430\u043B\u0438\u043B\u0441\u044F \u0438\u043B\u0438 \u043D\u0430\u043C\u0435\u0440\u0442\u0432\u043E \u0432\u0438\u0441\u0438\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u0438 \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u0435\u0433\u043E \u043E\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044C (\u0441\u0440\u043E\u043A\u0438 \u0438 \u0441\u0447\u0451\u0442\u0447\u0438\u043A \u2014 \u0432 \u0441\u043E\u0431\u044B\u0442\u0438\u044F\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430). \u0420\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0432\u043E\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D). \u041D\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0439 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0447\u0438\u043D\u0438\u0442\u044C \u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u043F\u0440\u043E \u044D\u0442\u043E \u0440\u043E\u0439 \u0443\u0436\u0435 \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C.` : (
|
|
16498
|
+
// TASWM-483: чеки, скорее всего, ЕСТЬ и даже зелёные — их просто не видно рою (403).
|
|
16499
|
+
// Врать «CI не прошёл» тут так же нельзя, как «CI зелёный»: единственная честная
|
|
16500
|
+
// формулировка — «не проверялся», и она обязана доехать до вердикта.
|
|
16501
|
+
over.kind === "denied" ? `\u26A0\uFE0F \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0440\u043E\u044E \u041D\u0415 \u0412\u0418\u0414\u041D\u042B: GitHub \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043E\u0442\u043A\u0430\u0437\u043E\u043C \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C \u2014 403${over.detail ? ` (${over.detail})` : ""}. \u0427\u0435\u043A\u0438 \u043D\u0430 PR, \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E, \u0435\u0441\u0442\u044C \u0438 \u0434\u0430\u0436\u0435 \u0437\u0435\u043B\u0451\u043D\u044B\u0435, \u043D\u043E \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0438\u0445 \u0440\u043E\u0439 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442, \u043F\u043E\u043A\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043D\u0435 \u0432\u044B\u0434\u0430\u0441\u0442 \u0434\u043E\u0441\u0442\u0443\u043F; \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u0440\u043E\u0439 \u0443\u0436\u0435 \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C. \u0420\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0432\u043E\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D), \u0438 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439 \xABCI \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0441\u044F: \u0440\u043E\u044E \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C (403)\xBB. \u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0447\u0438\u043D\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0441\u0438.` : `\u2705 \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u041B\u0418\u0421\u042C \u0438 \u043E\u043D\u0438 \u0437\u0435\u043B\u0451\u043D\u044B\u0435${counts} \u2014 \u0440\u043E\u0439 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E, \u0436\u0434\u0430\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0447\u0435\u0433\u043E.`
|
|
16502
|
+
);
|
|
16161
16503
|
return [
|
|
16162
16504
|
head,
|
|
16163
16505
|
"",
|
|
@@ -16627,6 +16969,27 @@ async function handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap)
|
|
|
16627
16969
|
return;
|
|
16628
16970
|
}
|
|
16629
16971
|
const ci = await deps.ci.checks(run.pr_url).catch(() => null);
|
|
16972
|
+
if (ci?.bucket === "denied") {
|
|
16973
|
+
const seen = ciDeniedStreak(deps.db, run.id) + 1;
|
|
16974
|
+
if (seen < CI_DENIED_LIMIT) {
|
|
16975
|
+
deps.db.addEvent(
|
|
16976
|
+
run.id,
|
|
16977
|
+
"ci_checked",
|
|
16978
|
+
`CI \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${CI_DENIED_MARK}${ci.detail ? ` \u2014 ${ci.detail}` : ""}; \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${seen}/${CI_DENIED_LIMIT} [${CI_WAIT_POLL_MARK}]`
|
|
16979
|
+
);
|
|
16980
|
+
return;
|
|
16981
|
+
}
|
|
16982
|
+
closeCiWatch(
|
|
16983
|
+
deps,
|
|
16984
|
+
run,
|
|
16985
|
+
CI_DENIED_STOP_MARK,
|
|
16986
|
+
`GitHub \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 403 \u043D\u0430 \u0447\u0442\u0435\u043D\u0438\u0435 \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432${ci.detail ? ` (${ci.detail})` : ""} \u2014 ${seen} \u043E\u043F\u0440\u043E\u0441(\u043E\u0432) \u043F\u043E\u0434\u0440\u044F\u0434. \u042D\u0442\u043E \u043D\u0435 \u0441\u0431\u043E\u0439 \u0441\u0435\u0442\u0438 \u0438 \u043D\u0435 \u043C\u0435\u0434\u043B\u0435\u043D\u043D\u044B\u0439 CI: \u0443 \u0440\u043E\u044F \u043D\u0435\u0442 \u043F\u0440\u0430\u0432 \u043D\u0430 \u0447\u0435\u043A\u0438 \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438, \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0442\u044C \u0431\u0435\u0441\u0441\u043C\u044B\u0441\u043B\u0435\u043D\u043D\u043E. \u041F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u044F \u043D\u0435 \u0432\u0438\u0436\u0443 \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u044E; \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0439\u0434\u0451\u0442 \u0411\u0415\u0417 CI (\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u043C\u0435\u0442\u0438\u0442 \u044D\u0442\u043E \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435), \u0434\u043E\u0441\u0442\u0443\u043F \u0432\u044B\u0434\u0430\u0451\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A: \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C GitHub App \u0440\u043E\u044F \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439/\u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044E.`,
|
|
16987
|
+
`${droneLabel(run)}: GitHub \u043D\u0435 \u0434\u0430\u0451\u0442 \u0440\u043E\u044E \u0447\u0438\u0442\u0430\u0442\u044C \u0447\u0435\u043A\u0438 (403) \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043D\u0435 \u0432\u0438\u0436\u0443, \u0437\u043E\u0432\u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0438 \u0432\u0435\u0434\u0443 \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 CI.`
|
|
16988
|
+
);
|
|
16989
|
+
await escalateCiDenied(deps, run, ci.detail ?? null, seen);
|
|
16990
|
+
await wakeReviewerOnCiReady(deps, run, { kind: "denied", ...ci.detail ? { detail: ci.detail } : {} });
|
|
16991
|
+
return;
|
|
16992
|
+
}
|
|
16630
16993
|
if (ci?.bucket === "none" && !repoHasCi(deps, run)) {
|
|
16631
16994
|
closeCiWatch(
|
|
16632
16995
|
deps,
|
|
@@ -16670,7 +17033,27 @@ function ciWatchMark(base3, cycle) {
|
|
|
16670
17033
|
return `${base3} [\u0446\u0438\u043A\u043B ${cycle}]`;
|
|
16671
17034
|
}
|
|
16672
17035
|
function isCiWatchClose(text) {
|
|
16673
|
-
return text.includes(CI_NO_CHECKS_MARK) || text.includes(CI_POLL_LIMIT_MARK);
|
|
17036
|
+
return text.includes(CI_NO_CHECKS_MARK) || text.includes(CI_POLL_LIMIT_MARK) || text.includes(CI_DENIED_STOP_MARK);
|
|
17037
|
+
}
|
|
17038
|
+
function ciDeniedStreak(db, runId) {
|
|
17039
|
+
return ciStreak(db, runId, CI_DENIED_MARK).count;
|
|
17040
|
+
}
|
|
17041
|
+
async function escalateCiDenied(deps, run, detail, polls) {
|
|
17042
|
+
const why = `${CI_DENIED_MARK}: GitHub \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 403 \u043D\u0430 \u0447\u0442\u0435\u043D\u0438\u0435 \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 PR${detail ? ` (${detail})` : ""} \u2014 ${polls} \u043E\u043F\u0440\u043E\u0441(\u043E\u0432) \u043F\u043E\u0434\u0440\u044F\u0434. \u0420\u043E\u0439 \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0442\u044C PR \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443\u043C\u0435\u0435\u0442, \u0430 \u0447\u0438\u0442\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u2014 \u043D\u0435\u0442: \u043F\u0440\u0430\u0432\u0430 \u0432\u044B\u0434\u0430\u043D\u044B \u0447\u0430\u0441\u0442\u0438\u0447\u043D\u043E. \u041E\u043F\u0440\u043E\u0441 CI \u043F\u043E \u044D\u0442\u043E\u043C\u0443 PR \u043F\u0440\u0435\u043A\u0440\u0430\u0449\u0451\u043D, \u0440\u0435\u0432\u044C\u044E \u0438\u0434\u0451\u0442 \u0431\u0435\u0437 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A. \u041D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A: \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C GitHub App \u0440\u043E\u044F \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439/\u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044E.`;
|
|
17043
|
+
deps.db.addEvent(run.id, "anomaly", why);
|
|
17044
|
+
if (!deps.watchdog?.notify) return;
|
|
17045
|
+
const signal = {
|
|
17046
|
+
key: `ci_denied:${run.id}:${run.review_cycle}`,
|
|
17047
|
+
title: signalTitle(run, "GitHub \u043D\u0435 \u0434\u0430\u0451\u0442 \u0440\u043E\u044E \u0447\u0438\u0442\u0430\u0442\u044C \u0447\u0435\u043A\u0438 (403)"),
|
|
17048
|
+
body: `${run.task_slug}: ${polls} \u043E\u043F\u0440\u043E\u0441(\u043E\u0432) \u043F\u043E\u0434\u0440\u044F\u0434 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0438\u0441\u044C 403${detail ? ` (${detail})` : ""} \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0440\u043E\u0439 \u043D\u0435 \u0432\u0438\u0434\u0438\u0442 \u0438 \u0436\u0434\u0430\u0442\u044C \u0438\u0445 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442. \u0420\u0435\u0432\u044C\u044E \u043F\u043E\u0439\u0434\u0451\u0442 \u0431\u0435\u0437 CI (\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u043C\u0435\u0442\u0438\u0442 \u044D\u0442\u043E \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435). \u0412\u044B\u0434\u0430\u0439 \u0434\u043E\u0441\u0442\u0443\u043F: GitHub App \u0440\u043E\u044F \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439/\u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044E. PR: ${run.pr_url ?? "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D"}.`,
|
|
17049
|
+
runId: run.id,
|
|
17050
|
+
...hrefOf(run.task_url)
|
|
17051
|
+
};
|
|
17052
|
+
try {
|
|
17053
|
+
await deps.watchdog.notify(signal);
|
|
17054
|
+
} catch (e) {
|
|
17055
|
+
console.error(`[overmind] \u0441\u0438\u0433\u043D\u0430\u043B \u043E\u0431 \u043E\u0442\u043A\u0430\u0437\u0435 GitHub \u043F\u043E \u0447\u0435\u043A\u0430\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B:`, e);
|
|
17056
|
+
}
|
|
16674
17057
|
}
|
|
16675
17058
|
function ciWatchClosed(db, run) {
|
|
16676
17059
|
return ciWatchCloseKind(db, run) != null;
|
|
@@ -16679,20 +17062,31 @@ var NO_CHECKS_STATUS = { bucket: "none", failing: [], failingRunIds: [], total:
|
|
|
16679
17062
|
function ciWatchCloseKind(db, run) {
|
|
16680
17063
|
const noChecks = ciWatchMark(CI_NO_CHECKS_MARK, run.review_cycle);
|
|
16681
17064
|
const gaveUp = ciWatchMark(CI_POLL_LIMIT_MARK, run.review_cycle);
|
|
17065
|
+
const denied = ciWatchMark(CI_DENIED_STOP_MARK, run.review_cycle);
|
|
16682
17066
|
for (const e of db.events(run.id)) {
|
|
16683
17067
|
const text = e.text ?? "";
|
|
16684
17068
|
if (text.includes(noChecks)) return { kind: "none", ci: NO_CHECKS_STATUS };
|
|
16685
17069
|
if (text.includes(gaveUp)) return { kind: "gaveup" };
|
|
17070
|
+
if (text.includes(denied)) {
|
|
17071
|
+
const detail = deniedDetailFrom(text);
|
|
17072
|
+
return { kind: "denied", ...detail ? { detail } : {} };
|
|
17073
|
+
}
|
|
16686
17074
|
}
|
|
16687
17075
|
return null;
|
|
16688
17076
|
}
|
|
17077
|
+
function deniedDetailFrom(text) {
|
|
17078
|
+
return text.match(/на чтение чек-ранов \(([^)]+)\)/)?.[1];
|
|
17079
|
+
}
|
|
16689
17080
|
function ciPollStreak(db, runId) {
|
|
17081
|
+
return ciStreak(db, runId, CI_WAIT_POLL_MARK);
|
|
17082
|
+
}
|
|
17083
|
+
function ciStreak(db, runId, mark) {
|
|
16690
17084
|
const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked" || isCiWatchClose(e.text ?? ""));
|
|
16691
17085
|
let count = 0;
|
|
16692
17086
|
let sinceMs = null;
|
|
16693
17087
|
for (let i = evs.length - 1; i >= 0; i -= 1) {
|
|
16694
17088
|
const e = evs[i];
|
|
16695
|
-
if (e.kind !== "ci_checked" || !(e.text ?? "").includes(
|
|
17089
|
+
if (e.kind !== "ci_checked" || !(e.text ?? "").includes(mark)) break;
|
|
16696
17090
|
count += 1;
|
|
16697
17091
|
sinceMs = sqliteMs(e.at);
|
|
16698
17092
|
}
|
|
@@ -16747,6 +17141,10 @@ function ciReadyLabel(over) {
|
|
|
16747
17141
|
// (isCiWatchClose/ciWatchCloseKind ищут его по подстроке), и событие о том, что
|
|
16748
17142
|
// побудка ждёт слота, не должно выглядеть вторым решением о закрытии.
|
|
16749
17143
|
"\u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0442\u0430\u043A \u0438 \u043D\u0435 \u0434\u043E\u0436\u0434\u0430\u043B\u0438\u0441\u044C, \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u043E\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044C CI"
|
|
17144
|
+
) : over.kind === "denied" ? (
|
|
17145
|
+
// По той же причине не дословно CI_DENIED_STOP_MARK (TASWM-483) — но «403» в тексте
|
|
17146
|
+
// остаётся: по журналу должна быть видна причина, а не «проверок нет».
|
|
17147
|
+
"\u0447\u0435\u043A\u0438 \u0440\u043E\u044E \u043D\u0435 \u0432\u0438\u0434\u043D\u044B: GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C (403)"
|
|
16750
17148
|
) : `CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 (${over.ci.pass}/${over.ci.total})`;
|
|
16751
17149
|
}
|
|
16752
17150
|
async function wakeReviewerOnCiReady(deps, run, over) {
|
|
@@ -17166,9 +17564,42 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
17166
17564
|
return "skipped";
|
|
17167
17565
|
}
|
|
17168
17566
|
if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
|
|
17567
|
+
if (e instanceof WorktreeBusyError && await healBusyWorktree(deps, run, e)) {
|
|
17568
|
+
try {
|
|
17569
|
+
await deps.spawn(run);
|
|
17570
|
+
if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
|
|
17571
|
+
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)} (\u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0437\u0430 \u043C\u0451\u0440\u0442\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439).`);
|
|
17572
|
+
return "spawned";
|
|
17573
|
+
} catch (again) {
|
|
17574
|
+
return await failBeforeStart(deps, run, String(again));
|
|
17575
|
+
}
|
|
17576
|
+
}
|
|
17169
17577
|
return await failBeforeStart(deps, run, String(e));
|
|
17170
17578
|
}
|
|
17171
17579
|
}
|
|
17580
|
+
async function healBusyWorktree(deps, run, e) {
|
|
17581
|
+
const path = e.worktreePath;
|
|
17582
|
+
if (!deps.releaseWorktree) return false;
|
|
17583
|
+
if (deps.liveWorktrees?.().has(path)) return false;
|
|
17584
|
+
const holder = deps.db.listRuns().find((r) => r.id !== run.id && r.worktree_path === path && !isTerminalStatus(r.status));
|
|
17585
|
+
if (holder && (holder.pid == null || deps.isAlive(holder.pid))) {
|
|
17586
|
+
return false;
|
|
17587
|
+
}
|
|
17588
|
+
if (deps.isDirty && await deps.isDirty(path).catch(() => true)) {
|
|
17589
|
+
deps.db.addEvent(
|
|
17590
|
+
run.id,
|
|
17591
|
+
"anomaly",
|
|
17592
|
+
`${DIRTY_WORKTREE_MARK}. \u041F\u0443\u0442\u044C: ${path}. \u0416\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043F\u0440\u0438\u0431\u0438\u0440\u0430\u0442\u044C \u043D\u0435 \u0441\u0442\u0430\u043B, \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C: \`git -C ${path} status --untracked-files=all\`.`
|
|
17593
|
+
);
|
|
17594
|
+
narrate(deps, `${run.task_slug}: worktree \u0437\u0430\u043D\u044F\u0442 \u043C\u0451\u0440\u0442\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439, \u043D\u043E \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
|
|
17595
|
+
return false;
|
|
17596
|
+
}
|
|
17597
|
+
await deps.releaseWorktree(run.repo, path).catch(() => {
|
|
17598
|
+
});
|
|
17599
|
+
deps.db.addEvent(run.id, "step", `worktree ${path} \u0434\u0435\u0440\u0436\u0430\u043B\u0430 \u043C\u0451\u0440\u0442\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u2014 \u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E \u0441\u043F\u0430\u0432\u043D (TASWM-476)`);
|
|
17600
|
+
if (holder) deps.db.addEvent(holder.id, "step", `worktree ${path} \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D: \u0436\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442, \u0441\u043B\u0430\u0433 \u043F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u0441\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id}`);
|
|
17601
|
+
return true;
|
|
17602
|
+
}
|
|
17172
17603
|
async function failBeforeStart(deps, run, reason) {
|
|
17173
17604
|
deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
|
|
17174
17605
|
deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
|
|
@@ -17733,6 +18164,18 @@ async function tickOnce(deps) {
|
|
|
17733
18164
|
}
|
|
17734
18165
|
}
|
|
17735
18166
|
}
|
|
18167
|
+
if (deps.reapWorktrees) {
|
|
18168
|
+
const rw = deps.reapWorktrees;
|
|
18169
|
+
const sinceLast = rw.lastAt == null ? Infinity : now - rw.lastAt;
|
|
18170
|
+
if (sinceLast >= rw.intervalMs) {
|
|
18171
|
+
rw.lastAt = now;
|
|
18172
|
+
try {
|
|
18173
|
+
await rw.run();
|
|
18174
|
+
} catch (e) {
|
|
18175
|
+
console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 worktree \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
|
|
18176
|
+
}
|
|
18177
|
+
}
|
|
18178
|
+
}
|
|
17736
18179
|
if (deps.logRotate) {
|
|
17737
18180
|
const lr = deps.logRotate;
|
|
17738
18181
|
const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
|
|
@@ -17969,6 +18412,8 @@ async function tickOnce(deps) {
|
|
|
17969
18412
|
}
|
|
17970
18413
|
}
|
|
17971
18414
|
await closeMergedTaskRuns(deps);
|
|
18415
|
+
await rescueReviewlessPrs(deps, stormActive, now);
|
|
18416
|
+
relinkUnboundReviewers(deps);
|
|
17972
18417
|
await closeOrphanedReviewers(deps);
|
|
17973
18418
|
await spawnVerifiersForOpenPrs(deps, stormActive);
|
|
17974
18419
|
refreshReviewPhases(deps);
|
|
@@ -18130,8 +18575,8 @@ async function tickOnce(deps) {
|
|
|
18130
18575
|
const ciWatchOff = parent != null && ciWatchClosed(deps.db, parent);
|
|
18131
18576
|
const ci = ciWatchOff ? null : await deps.ci.checks(run.pr_url).catch(() => null);
|
|
18132
18577
|
const noCiRepo = ci?.bucket === "none" && !repoHasCi(deps, run);
|
|
18133
|
-
if (!ciWatchOff && !noCiRepo && (ci == null || ci.bucket === "pending" || ci.bucket === "none")) {
|
|
18134
|
-
const what = ci == null ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0440\u043E \u0447\u0435\u043A\u0438 \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 \u043F\u0440\u043E\u044F\u0441\u043D\u0438\u0442\u0441\u044F" : ci.bucket === "none" ? "\u0447\u0435\u043A\u043E\u0432 \u043D\u0430 PR \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0447\u0435\u043A-\u0440\u0430\u043D\u0430, \u043D\u043E workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C" : `CI ${ci.pass}/${ci.total}`;
|
|
18578
|
+
if (!ciWatchOff && !noCiRepo && (ci == null || ci.bucket === "pending" || ci.bucket === "none" || ci.bucket === "denied")) {
|
|
18579
|
+
const what = ci == null ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0440\u043E \u0447\u0435\u043A\u0438 \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 \u043F\u0440\u043E\u044F\u0441\u043D\u0438\u0442\u0441\u044F" : ci.bucket === "none" ? "\u0447\u0435\u043A\u043E\u0432 \u043D\u0430 PR \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0447\u0435\u043A-\u0440\u0430\u043D\u0430, \u043D\u043E workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C" : ci.bucket === "denied" ? `\u0447\u0435\u043A\u0438 \u0440\u043E\u044E \u043D\u0435 \u0432\u0438\u0434\u043D\u044B: GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C (403)${ci.detail ? ` \u2014 ${ci.detail}` : ""}` : `CI ${ci.pass}/${ci.total}`;
|
|
18135
18580
|
deps.db.updateRun(run.id, { status: "paused", stop_reason: AWAITING_CI_REASON });
|
|
18136
18581
|
deps.db.addEvent(run.id, "paused", `${AWAITING_CI_REASON} (${what}) \u2014 \u043D\u0435 \u0441\u043C\u0435\u0440\u0442\u044C: \u0440\u0430\u0437\u0431\u0443\u0436\u0443 \u0441\u0430\u043C, \u043A\u043E\u0433\u0434\u0430 \u0447\u0435\u043A\u0438 \u0434\u043E\u0439\u0434\u0443\u0442 \u0434\u043E \u043A\u043E\u043D\u0446\u0430`);
|
|
18137
18582
|
narrate(deps, `${droneLabel(run)} \u0443\u0448\u0451\u043B \u0436\u0434\u0430\u0442\u044C CI (${what}) \u2014 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u044E, \u0440\u0430\u0437\u0431\u0443\u0436\u0443 \u043F\u043E \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A.`);
|
|
@@ -19212,7 +19657,18 @@ async function runDaemon(opts) {
|
|
|
19212
19657
|
// TASWM-472: третья ось — ОЧЕРЕДЬ. Вливаем по одному и только на свежей базе; ридер
|
|
19213
19658
|
// свежести ходит в gh (`compare/base...head` → behind_by). Ключа нет при
|
|
19214
19659
|
// SWARM_MERGE_QUEUE=0 — тогда гейт отвечает ровно как до TASWM-472.
|
|
19215
|
-
|
|
19660
|
+
// TASWM-483: отказ по правам (403) очереди мержа приходит как «спросить не удалось» (null),
|
|
19661
|
+
// а не как отдельная ось: для НЕЁ это одно и то же — цвет проверок неизвестен, значит
|
|
19662
|
+
// вливать нельзя (checks_unknown, см. докблок computeMergeQueue про несимметричную цену).
|
|
19663
|
+
// Отличать 403 от таймаута нужно тому, кто ЖДЁТ и повторяет (handleCi), а очередь не ждёт.
|
|
19664
|
+
...SWARM_MERGE_QUEUE ? {
|
|
19665
|
+
baseSync: (url) => prBaseSync(url),
|
|
19666
|
+
checks: async (url) => {
|
|
19667
|
+
const ci = await prCiStatus(url);
|
|
19668
|
+
if (ci == null || ci.bucket === "denied") return null;
|
|
19669
|
+
return { bucket: ci.bucket };
|
|
19670
|
+
}
|
|
19671
|
+
} : {}
|
|
19216
19672
|
});
|
|
19217
19673
|
const deps = {
|
|
19218
19674
|
db,
|
|
@@ -19224,7 +19680,9 @@ async function runDaemon(opts) {
|
|
|
19224
19680
|
const repoOfRun = normalizeRepo(run.repo || opts.repo);
|
|
19225
19681
|
const taskClient = tlFor(run.workspace_id);
|
|
19226
19682
|
const [rawTask, verifyMode, projectBase, zoneNotes] = await Promise.all([
|
|
19227
|
-
|
|
19683
|
+
// TASWM-484: с причиной — на этом чтении стоит fail-loud ниже, и именно его
|
|
19684
|
+
// сообщение читает человек, когда дрон не поднялся.
|
|
19685
|
+
taskClient ? taskClient.getTaskDetailed(run.task_slug) : Promise.resolve(null),
|
|
19228
19686
|
verifyModeReader ? verifyModeReader(repoOfRun) : Promise.resolve(void 0),
|
|
19229
19687
|
baseBranchReader ? baseBranchReader(repoOfRun) : Promise.resolve(""),
|
|
19230
19688
|
// TASWM-466: память зоны прогона — тем же параллельным чтением, что и остальные
|
|
@@ -19362,6 +19820,32 @@ async function runDaemon(opts) {
|
|
|
19362
19820
|
intervalMs: SWARM_SWEEP_INTERVAL_MS,
|
|
19363
19821
|
lastAt: null
|
|
19364
19822
|
},
|
|
19823
|
+
// Жнец worktree (TASWM-476): убирает копии терминальных прогонов, чьи PR смержены/закрыты.
|
|
19824
|
+
// Живость каталога определяем ПО ПРОЦЕССАМ (listDroneProcesses — тот же снимок, по которому
|
|
19825
|
+
// идёт реконсайл рантайма), а не только по статусу в зеркале: статус мог не успеть
|
|
19826
|
+
// обновиться, а сносить каталог из-под работающего дрона нельзя ни при каком статусе.
|
|
19827
|
+
reapWorktrees: {
|
|
19828
|
+
run: async () => {
|
|
19829
|
+
const report = await reapFinishedWorktrees({
|
|
19830
|
+
db,
|
|
19831
|
+
prStatus: (url) => prStatus(url),
|
|
19832
|
+
isDirty: (p) => worktreeDirty(p),
|
|
19833
|
+
releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
|
|
19834
|
+
pruneWorktrees: (repo) => pruneWorktrees(repo || opts.repo),
|
|
19835
|
+
worktreeExists: existsSync17,
|
|
19836
|
+
liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
|
|
19837
|
+
narrate: narrator.narrate
|
|
19838
|
+
});
|
|
19839
|
+
if (report.removed || report.keptDirty) {
|
|
19840
|
+
console.log(`[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 worktree: \u0441\u043D\u0435\u0441\u0435\u043D\u043E ${report.removed}, \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E \u0441 \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 ${report.keptDirty}`);
|
|
19841
|
+
}
|
|
19842
|
+
},
|
|
19843
|
+
intervalMs: SWARM_WORKTREE_REAP_INTERVAL_MS,
|
|
19844
|
+
lastAt: null
|
|
19845
|
+
},
|
|
19846
|
+
// Самолечение залипшего слага (TASWM-476): те же два кирпича, что у жнеца выше.
|
|
19847
|
+
releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
|
|
19848
|
+
liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
|
|
19365
19849
|
isDirty: (p) => worktreeDirty(p),
|
|
19366
19850
|
// git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
|
|
19367
19851
|
lastCommitMs: (p) => worktreeLastCommitMs(p),
|
|
@@ -19455,7 +19939,7 @@ async function runDaemon(opts) {
|
|
|
19455
19939
|
// fresh-оживление строит промпт с нуля, и без тела задачи это тот же слепой дрон.
|
|
19456
19940
|
getTask: async (slug, ws) => {
|
|
19457
19941
|
const client = tlFor(ws);
|
|
19458
|
-
return client ? requireTaskBody(await client.
|
|
19942
|
+
return client ? requireTaskBody(await client.getTaskDetailed(slug), slug, ws) : null;
|
|
19459
19943
|
},
|
|
19460
19944
|
...verifyModeReader ? { verifyMode: verifyModeReader } : {},
|
|
19461
19945
|
// TASWM-466: тот же ридер памяти зоны, что на спавне — оживлённый дрон строит
|
|
@@ -19906,6 +20390,9 @@ export {
|
|
|
19906
20390
|
CHANGES_WAKE_LIMIT,
|
|
19907
20391
|
CHANGES_WAKE_MARK,
|
|
19908
20392
|
CI_CHECK_COOLDOWN_MS,
|
|
20393
|
+
CI_DENIED_LIMIT,
|
|
20394
|
+
CI_DENIED_MARK,
|
|
20395
|
+
CI_DENIED_STOP_MARK,
|
|
19909
20396
|
CI_NO_CHECKS_MARK,
|
|
19910
20397
|
CI_POLL_LIMIT,
|
|
19911
20398
|
CI_POLL_LIMIT_MARK,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskless-app/swarm",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"execa": "^10.0.0",
|
|
39
39
|
"zod": "3.25.76"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "584411cbe69583a8727e89caa8ae7e626f8fd8ca",
|
|
42
42
|
"scripts": {
|
|
43
43
|
"overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
|
|
44
44
|
"swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",
|