@taskless-app/swarm 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/cli.js +436 -84
- package/dist/overmind.js +804 -225
- package/package.json +1 -11
package/dist/overmind.js
CHANGED
|
@@ -758,6 +758,7 @@ __export(config_exports, {
|
|
|
758
758
|
RUN_LOGS_DIR: () => RUN_LOGS_DIR,
|
|
759
759
|
SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
760
760
|
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
761
|
+
SWARM_ANOMALY_MAX_REPEATS: () => SWARM_ANOMALY_MAX_REPEATS,
|
|
761
762
|
SWARM_ANOMALY_OUTBOX_STUCK_MS: () => SWARM_ANOMALY_OUTBOX_STUCK_MS,
|
|
762
763
|
SWARM_ANOMALY_PR_NO_REVIEWER_MS: () => SWARM_ANOMALY_PR_NO_REVIEWER_MS,
|
|
763
764
|
SWARM_ANOMALY_PR_STUCK_MERGE_MS: () => SWARM_ANOMALY_PR_STUCK_MERGE_MS,
|
|
@@ -765,6 +766,7 @@ __export(config_exports, {
|
|
|
765
766
|
SWARM_ANOMALY_QUEUE_STALLED_MS: () => SWARM_ANOMALY_QUEUE_STALLED_MS,
|
|
766
767
|
SWARM_ANOMALY_RENOTIFY_MS: () => SWARM_ANOMALY_RENOTIFY_MS,
|
|
767
768
|
SWARM_ANOMALY_RESOURCE_GATE_STALL_MS: () => SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
|
|
769
|
+
SWARM_ANOMALY_RUN_DOWN_WINDOW_MS: () => SWARM_ANOMALY_RUN_DOWN_WINDOW_MS,
|
|
768
770
|
SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
|
|
769
771
|
SWARM_BACKEND: () => SWARM_BACKEND,
|
|
770
772
|
SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
|
|
@@ -890,7 +892,7 @@ function reviewCutoffAt() {
|
|
|
890
892
|
}
|
|
891
893
|
return at;
|
|
892
894
|
}
|
|
893
|
-
var SWARM_HOME, DB_PATH, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_REPO_PATHS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
|
|
895
|
+
var SWARM_HOME, DB_PATH, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_REPO_PATHS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
|
|
894
896
|
var init_config = __esm({
|
|
895
897
|
"src/config.ts"() {
|
|
896
898
|
"use strict";
|
|
@@ -954,6 +956,8 @@ var init_config = __esm({
|
|
|
954
956
|
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
955
957
|
WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
956
958
|
SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
|
|
959
|
+
SWARM_ANOMALY_MAX_REPEATS = Math.trunc(envNum("SWARM_ANOMALY_MAX_REPEATS", 3));
|
|
960
|
+
SWARM_ANOMALY_RUN_DOWN_WINDOW_MS = envNum("SWARM_ANOMALY_RUN_DOWN_WINDOW_MS", 24 * 60 * 6e4);
|
|
957
961
|
SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
|
|
958
962
|
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
|
|
959
963
|
SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
|
|
@@ -1909,7 +1913,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
1909
1913
|
// src/resources.ts
|
|
1910
1914
|
import os from "os";
|
|
1911
1915
|
import { execFileSync } from "child_process";
|
|
1912
|
-
import { readFileSync as
|
|
1916
|
+
import { readFileSync as readFileSync3, statfsSync } from "fs";
|
|
1913
1917
|
function isTransientResourceError(e) {
|
|
1914
1918
|
const code = e?.code;
|
|
1915
1919
|
return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
|
|
@@ -1967,7 +1971,7 @@ function fileDescriptorLoadRatio() {
|
|
|
1967
1971
|
}
|
|
1968
1972
|
if (process.platform === "linux") {
|
|
1969
1973
|
try {
|
|
1970
|
-
const [allocated, , max] =
|
|
1974
|
+
const [allocated, , max] = readFileSync3("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
|
|
1971
1975
|
if (Number.isFinite(allocated ?? NaN) && Number.isFinite(max ?? NaN) && max) return allocated / max;
|
|
1972
1976
|
} catch {
|
|
1973
1977
|
}
|
|
@@ -2103,15 +2107,15 @@ var init_resources = __esm({
|
|
|
2103
2107
|
});
|
|
2104
2108
|
|
|
2105
2109
|
// src/heartbeat.ts
|
|
2106
|
-
import { readFileSync as
|
|
2110
|
+
import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2107
2111
|
function writeHeartbeat(path, now) {
|
|
2108
2112
|
const tmp = `${path}.tmp`;
|
|
2109
|
-
|
|
2110
|
-
|
|
2113
|
+
writeFileSync4(tmp, String(now));
|
|
2114
|
+
renameSync3(tmp, path);
|
|
2111
2115
|
}
|
|
2112
2116
|
function readHeartbeat(path) {
|
|
2113
2117
|
try {
|
|
2114
|
-
const ms = Number(
|
|
2118
|
+
const ms = Number(readFileSync5(path, "utf8").trim());
|
|
2115
2119
|
return Number.isFinite(ms) ? ms : null;
|
|
2116
2120
|
} catch {
|
|
2117
2121
|
return null;
|
|
@@ -2447,16 +2451,16 @@ var init_procCleanup = __esm({
|
|
|
2447
2451
|
});
|
|
2448
2452
|
|
|
2449
2453
|
// src/git.ts
|
|
2450
|
-
import { existsSync as
|
|
2454
|
+
import { existsSync as existsSync8, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
|
|
2451
2455
|
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
2452
|
-
import { isAbsolute, join as
|
|
2456
|
+
import { isAbsolute, join as join9 } from "path";
|
|
2453
2457
|
import { $ as $2 } from "execa";
|
|
2454
2458
|
async function reclaimStaleWorktree(repo, worktree) {
|
|
2455
2459
|
await detachWorktree(repo, worktree);
|
|
2456
2460
|
}
|
|
2457
2461
|
function isLinkedWorktree(dir) {
|
|
2458
2462
|
try {
|
|
2459
|
-
return statSync4(
|
|
2463
|
+
return statSync4(join9(dir, ".git")).isFile();
|
|
2460
2464
|
} catch {
|
|
2461
2465
|
return false;
|
|
2462
2466
|
}
|
|
@@ -2468,7 +2472,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
2468
2472
|
}
|
|
2469
2473
|
}
|
|
2470
2474
|
async function ensureWorktreeDirGone(worktree) {
|
|
2471
|
-
if (!
|
|
2475
|
+
if (!existsSync8(worktree)) return false;
|
|
2472
2476
|
if (!isLinkedWorktree(worktree)) {
|
|
2473
2477
|
console.error(
|
|
2474
2478
|
`[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
|
|
@@ -2483,7 +2487,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
2483
2487
|
);
|
|
2484
2488
|
return true;
|
|
2485
2489
|
}
|
|
2486
|
-
if (
|
|
2490
|
+
if (existsSync8(worktree)) {
|
|
2487
2491
|
console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
|
|
2488
2492
|
}
|
|
2489
2493
|
return true;
|
|
@@ -2526,7 +2530,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
|
|
|
2526
2530
|
}
|
|
2527
2531
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
2528
2532
|
if (busyAt) {
|
|
2529
|
-
const exists =
|
|
2533
|
+
const exists = existsSync8(busyAt);
|
|
2530
2534
|
const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
|
|
2531
2535
|
if (live) throw new WorktreeBusyError(branch, busyAt);
|
|
2532
2536
|
if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
|
|
@@ -2562,9 +2566,9 @@ async function ensureGitExcluded(worktree, pattern) {
|
|
|
2562
2566
|
const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
|
|
2563
2567
|
const raw = out.stdout.trim();
|
|
2564
2568
|
if (out.exitCode !== 0 || !raw) return false;
|
|
2565
|
-
const commonDir = isAbsolute(raw) ? raw :
|
|
2566
|
-
const infoDir =
|
|
2567
|
-
const excludeFile =
|
|
2569
|
+
const commonDir = isAbsolute(raw) ? raw : join9(worktree, raw);
|
|
2570
|
+
const infoDir = join9(commonDir, "info");
|
|
2571
|
+
const excludeFile = join9(infoDir, "exclude");
|
|
2568
2572
|
let current = "";
|
|
2569
2573
|
try {
|
|
2570
2574
|
current = await readFile(excludeFile, "utf8");
|
|
@@ -2597,7 +2601,7 @@ async function worktreeDirty(path) {
|
|
|
2597
2601
|
async function repairMovedWorktrees(targetDir) {
|
|
2598
2602
|
const subdirs = (dir) => {
|
|
2599
2603
|
try {
|
|
2600
|
-
return readdirSync7(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) =>
|
|
2604
|
+
return readdirSync7(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join9(dir, d.name));
|
|
2601
2605
|
} catch {
|
|
2602
2606
|
return [];
|
|
2603
2607
|
}
|
|
@@ -2709,18 +2713,18 @@ __export(droneChannel_exports, {
|
|
|
2709
2713
|
verifyLocalCliToken: () => verifyLocalCliToken
|
|
2710
2714
|
});
|
|
2711
2715
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
2712
|
-
import { chmodSync, mkdirSync as mkdirSync5, readFileSync as
|
|
2716
|
+
import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
2713
2717
|
import { dirname as dirname3 } from "path";
|
|
2714
2718
|
import { serve } from "@hono/node-server";
|
|
2715
2719
|
function loadChannelSecret(path) {
|
|
2716
2720
|
try {
|
|
2717
|
-
const s =
|
|
2721
|
+
const s = readFileSync10(path, "utf8").trim();
|
|
2718
2722
|
if (s.length >= 32) return s;
|
|
2719
2723
|
} catch {
|
|
2720
2724
|
}
|
|
2721
2725
|
const secret = randomBytes(32).toString("hex");
|
|
2722
2726
|
mkdirSync5(dirname3(path), { recursive: true });
|
|
2723
|
-
|
|
2727
|
+
writeFileSync7(path, secret, { mode: 384 });
|
|
2724
2728
|
try {
|
|
2725
2729
|
chmodSync(path, 384);
|
|
2726
2730
|
} catch {
|
|
@@ -3050,7 +3054,7 @@ var init_droneChannel = __esm({
|
|
|
3050
3054
|
|
|
3051
3055
|
// src/attachments.ts
|
|
3052
3056
|
import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
|
|
3053
|
-
import { join as
|
|
3057
|
+
import { join as join11 } from "path";
|
|
3054
3058
|
function attachmentsFromDoc(doc, origin) {
|
|
3055
3059
|
const out = [];
|
|
3056
3060
|
const walk = (node) => {
|
|
@@ -3141,7 +3145,7 @@ async function materializeAttachments(opts) {
|
|
|
3141
3145
|
};
|
|
3142
3146
|
const result = { files: [], skipped: [] };
|
|
3143
3147
|
if (opts.refs.length === 0) return result;
|
|
3144
|
-
const dir =
|
|
3148
|
+
const dir = join11(opts.worktree, ATTACHMENTS_DIR);
|
|
3145
3149
|
await io.removeDir(dir);
|
|
3146
3150
|
let dirReady = false;
|
|
3147
3151
|
let total = 0;
|
|
@@ -3187,7 +3191,7 @@ async function materializeAttachments(opts) {
|
|
|
3187
3191
|
await io.mkdir(dir);
|
|
3188
3192
|
dirReady = true;
|
|
3189
3193
|
}
|
|
3190
|
-
await io.writeFile(
|
|
3194
|
+
await io.writeFile(join11(dir, name), file.bytes);
|
|
3191
3195
|
} catch (e) {
|
|
3192
3196
|
result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
|
|
3193
3197
|
continue;
|
|
@@ -3340,7 +3344,7 @@ var init_attachments = __esm({
|
|
|
3340
3344
|
|
|
3341
3345
|
// src/shotUpload.ts
|
|
3342
3346
|
import { readFile as fsReadFile } from "fs/promises";
|
|
3343
|
-
import { basename as basename2, isAbsolute as isAbsolute2, join as
|
|
3347
|
+
import { basename as basename2, isAbsolute as isAbsolute2, join as join12 } from "path";
|
|
3344
3348
|
function shotMimetype(path) {
|
|
3345
3349
|
const dot = path.lastIndexOf(".");
|
|
3346
3350
|
if (dot < 0) return null;
|
|
@@ -3366,7 +3370,7 @@ async function readShots(paths, opts) {
|
|
|
3366
3370
|
}
|
|
3367
3371
|
let bytes;
|
|
3368
3372
|
try {
|
|
3369
|
-
bytes = await read(isAbsolute2(path) ? path :
|
|
3373
|
+
bytes = await read(isAbsolute2(path) ? path : join12(opts.worktree, path));
|
|
3370
3374
|
} catch (e) {
|
|
3371
3375
|
skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
|
|
3372
3376
|
continue;
|
|
@@ -3444,8 +3448,8 @@ var init_shotUpload = __esm({
|
|
|
3444
3448
|
|
|
3445
3449
|
// src/oauth.ts
|
|
3446
3450
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
3447
|
-
import { readFileSync as
|
|
3448
|
-
import { join as
|
|
3451
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
3452
|
+
import { join as join13 } from "path";
|
|
3449
3453
|
function makePkce() {
|
|
3450
3454
|
const verifier = randomBytes2(48).toString("base64url");
|
|
3451
3455
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
@@ -3529,23 +3533,23 @@ function toStore(clientId, t, now) {
|
|
|
3529
3533
|
}
|
|
3530
3534
|
function loadStore() {
|
|
3531
3535
|
try {
|
|
3532
|
-
return JSON.parse(
|
|
3536
|
+
return JSON.parse(readFileSync11(STORE_PATH, "utf8"));
|
|
3533
3537
|
} catch {
|
|
3534
3538
|
return null;
|
|
3535
3539
|
}
|
|
3536
3540
|
}
|
|
3537
3541
|
function saveStore(s) {
|
|
3538
|
-
|
|
3542
|
+
writeFileSync8(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
|
|
3539
3543
|
}
|
|
3540
3544
|
function clearStore() {
|
|
3541
3545
|
try {
|
|
3542
|
-
|
|
3546
|
+
unlinkSync2(STORE_PATH);
|
|
3543
3547
|
} catch {
|
|
3544
3548
|
}
|
|
3545
3549
|
}
|
|
3546
3550
|
function loadClientId() {
|
|
3547
3551
|
try {
|
|
3548
|
-
return JSON.parse(
|
|
3552
|
+
return JSON.parse(readFileSync11(CLIENT_PATH, "utf8")).clientId;
|
|
3549
3553
|
} catch {
|
|
3550
3554
|
return null;
|
|
3551
3555
|
}
|
|
@@ -3554,7 +3558,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
|
|
|
3554
3558
|
const existing = loadClientId();
|
|
3555
3559
|
if (existing) return existing;
|
|
3556
3560
|
const clientId = await registerClient({ api, redirectUri }, fetchFn);
|
|
3557
|
-
|
|
3561
|
+
writeFileSync8(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
|
|
3558
3562
|
return clientId;
|
|
3559
3563
|
}
|
|
3560
3564
|
async function ensureAccess(store, opts = {}) {
|
|
@@ -3580,18 +3584,18 @@ var init_oauth = __esm({
|
|
|
3580
3584
|
AUTHORIZE = "/api/oauth/authorize";
|
|
3581
3585
|
TOKEN = "/api/oauth/token";
|
|
3582
3586
|
SCOPE = "openid profile email taskless:full";
|
|
3583
|
-
STORE_PATH =
|
|
3584
|
-
CLIENT_PATH =
|
|
3587
|
+
STORE_PATH = join13(SWARM_HOME, "oauth.json");
|
|
3588
|
+
CLIENT_PATH = join13(SWARM_HOME, "oauth-client.json");
|
|
3585
3589
|
base = (api) => api.replace(/\/$/, "");
|
|
3586
3590
|
}
|
|
3587
3591
|
});
|
|
3588
3592
|
|
|
3589
3593
|
// src/agentToken.ts
|
|
3590
|
-
import { readFileSync as
|
|
3591
|
-
import { join as
|
|
3594
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3 } from "fs";
|
|
3595
|
+
import { join as join14 } from "path";
|
|
3592
3596
|
function loadAgentToken() {
|
|
3593
3597
|
try {
|
|
3594
|
-
const s = JSON.parse(
|
|
3598
|
+
const s = JSON.parse(readFileSync12(STORE_PATH2, "utf8"));
|
|
3595
3599
|
return s.token ? s : null;
|
|
3596
3600
|
} catch {
|
|
3597
3601
|
return null;
|
|
@@ -3628,17 +3632,17 @@ var init_agentToken = __esm({
|
|
|
3628
3632
|
"src/agentToken.ts"() {
|
|
3629
3633
|
"use strict";
|
|
3630
3634
|
init_config();
|
|
3631
|
-
STORE_PATH2 =
|
|
3635
|
+
STORE_PATH2 = join14(SWARM_HOME, "agent.json");
|
|
3632
3636
|
base2 = (api) => api.replace(/\/$/, "");
|
|
3633
3637
|
}
|
|
3634
3638
|
});
|
|
3635
3639
|
|
|
3636
3640
|
// src/workspace.ts
|
|
3637
|
-
import { readFileSync as
|
|
3638
|
-
import { join as
|
|
3641
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
|
|
3642
|
+
import { join as join15 } from "path";
|
|
3639
3643
|
function loadWorkspace() {
|
|
3640
3644
|
try {
|
|
3641
|
-
const w = JSON.parse(
|
|
3645
|
+
const w = JSON.parse(readFileSync13(WORKSPACE_PATH, "utf8"));
|
|
3642
3646
|
return w && typeof w.id === "string" && w.id ? w : null;
|
|
3643
3647
|
} catch {
|
|
3644
3648
|
return null;
|
|
@@ -3652,7 +3656,7 @@ var init_workspace = __esm({
|
|
|
3652
3656
|
"src/workspace.ts"() {
|
|
3653
3657
|
"use strict";
|
|
3654
3658
|
init_config();
|
|
3655
|
-
WORKSPACE_PATH =
|
|
3659
|
+
WORKSPACE_PATH = join15(SWARM_HOME, "workspace.json");
|
|
3656
3660
|
}
|
|
3657
3661
|
});
|
|
3658
3662
|
|
|
@@ -4116,8 +4120,8 @@ var init_droneStore = __esm({
|
|
|
4116
4120
|
|
|
4117
4121
|
// src/overmind.ts
|
|
4118
4122
|
init_db();
|
|
4119
|
-
import { existsSync as
|
|
4120
|
-
import { basename as basename4, dirname as dirname5, join as
|
|
4123
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15, readdirSync as readdirSync9, renameSync as renameSync7 } from "fs";
|
|
4124
|
+
import { basename as basename4, dirname as dirname5, join as join19 } from "path";
|
|
4121
4125
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
4122
4126
|
import { spawn as spawn3 } from "child_process";
|
|
4123
4127
|
|
|
@@ -4127,7 +4131,69 @@ function liveSessionIds(db, isAlive) {
|
|
|
4127
4131
|
return db.listRuns().filter((r) => !isTerminalStatus(r.status) && r.pid != null && isAlive(r.pid)).map((r) => r.id);
|
|
4128
4132
|
}
|
|
4129
4133
|
function initialRestartState() {
|
|
4130
|
-
return { phase: "idle", requestedAt: null };
|
|
4134
|
+
return { phase: "idle", requestedAt: null, intent: "restart" };
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
// src/pauseState.ts
|
|
4138
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4139
|
+
import { join as join2 } from "path";
|
|
4140
|
+
function pauseStatePath(home) {
|
|
4141
|
+
return join2(home, "pause.json");
|
|
4142
|
+
}
|
|
4143
|
+
var ACTIVE = { paused: false, at: "", source: "cli" };
|
|
4144
|
+
function loadPauseState(home, io = {}) {
|
|
4145
|
+
const exists = io.exists ?? existsSync2;
|
|
4146
|
+
const readFile2 = io.readFile ?? ((p) => readFileSync2(p, "utf8"));
|
|
4147
|
+
const path = pauseStatePath(home);
|
|
4148
|
+
if (!exists(path)) return ACTIVE;
|
|
4149
|
+
try {
|
|
4150
|
+
const parsed = JSON.parse(readFile2(path));
|
|
4151
|
+
if (parsed.paused !== true) return ACTIVE;
|
|
4152
|
+
return {
|
|
4153
|
+
paused: true,
|
|
4154
|
+
at: typeof parsed.at === "string" ? parsed.at : "",
|
|
4155
|
+
source: parsed.source === "web" || parsed.source === "remote" ? parsed.source : "cli",
|
|
4156
|
+
...typeof parsed.reason === "string" && parsed.reason.trim() ? { reason: parsed.reason.trim() } : {}
|
|
4157
|
+
};
|
|
4158
|
+
} catch {
|
|
4159
|
+
return ACTIVE;
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
function savePauseState(home, state, io = {}) {
|
|
4163
|
+
const path = pauseStatePath(home);
|
|
4164
|
+
if (!state.paused) {
|
|
4165
|
+
const remove = io.remove ?? ((p) => existsSync2(p) && unlinkSync(p));
|
|
4166
|
+
remove(path);
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
const writeFile2 = io.writeFile ?? ((p, data) => {
|
|
4170
|
+
writeFileSync2(`${p}.tmp`, data);
|
|
4171
|
+
renameSync2(`${p}.tmp`, p);
|
|
4172
|
+
});
|
|
4173
|
+
writeFile2(path, JSON.stringify(state, null, 2));
|
|
4174
|
+
}
|
|
4175
|
+
function makePauseState(source, reason, now = Date.now()) {
|
|
4176
|
+
return {
|
|
4177
|
+
paused: true,
|
|
4178
|
+
at: new Date(now).toISOString(),
|
|
4179
|
+
source,
|
|
4180
|
+
...reason && reason.trim() ? { reason: reason.trim() } : {}
|
|
4181
|
+
};
|
|
4182
|
+
}
|
|
4183
|
+
function pauseText(state, now = Date.now()) {
|
|
4184
|
+
if (!state?.paused) return "\u0440\u043E\u0439 \u0430\u043A\u0442\u0438\u0432\u0435\u043D \u2014 \u0431\u0435\u0440\u0451\u0442 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443";
|
|
4185
|
+
const since = state.at ? sinceText(state.at, now) : "";
|
|
4186
|
+
const who = { cli: "\u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435", web: "\u0441 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0439 \u043C\u043E\u0440\u0434\u044B", remote: "\u0438\u0437 Taskless" }[state.source];
|
|
4187
|
+
const why = state.reason ? ` \u2014 ${state.reason}` : "";
|
|
4188
|
+
return `\u0440\u043E\u0439 \u041D\u0410 \u041F\u0410\u0423\u0417\u0415${since} (${who})${why}`;
|
|
4189
|
+
}
|
|
4190
|
+
function sinceText(at, now) {
|
|
4191
|
+
const ms = Date.parse(at);
|
|
4192
|
+
if (Number.isNaN(ms)) return "";
|
|
4193
|
+
const min = Math.max(0, Math.round((now - ms) / 6e4));
|
|
4194
|
+
if (min < 1) return " (\u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E)";
|
|
4195
|
+
if (min < 60) return ` (${min} \u043C\u0438\u043D)`;
|
|
4196
|
+
return ` (${Math.round(min / 60)} \u0447)`;
|
|
4131
4197
|
}
|
|
4132
4198
|
|
|
4133
4199
|
// src/swarmClient.ts
|
|
@@ -4135,13 +4201,13 @@ init_config();
|
|
|
4135
4201
|
init_resources();
|
|
4136
4202
|
init_names2();
|
|
4137
4203
|
init_storm();
|
|
4138
|
-
import { appendFileSync, existsSync as
|
|
4204
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4139
4205
|
import { dirname } from "path";
|
|
4140
4206
|
import { randomUUID } from "crypto";
|
|
4141
4207
|
|
|
4142
4208
|
// src/logTail.ts
|
|
4143
|
-
import { closeSync, existsSync as
|
|
4144
|
-
import { join as
|
|
4209
|
+
import { closeSync, existsSync as existsSync3, openSync, readSync, readdirSync, statSync } from "fs";
|
|
4210
|
+
import { join as join3 } from "path";
|
|
4145
4211
|
var TAIL_LINES_FALLBACK = 200;
|
|
4146
4212
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
4147
4213
|
function fileHeader(name, size, shown) {
|
|
@@ -4182,7 +4248,7 @@ function readTail(path, opts) {
|
|
|
4182
4248
|
return { text, size, shown: Buffer.byteLength(text, "utf8") };
|
|
4183
4249
|
}
|
|
4184
4250
|
function latestRunLaunch(runLogsDir, localRunId) {
|
|
4185
|
-
const dir =
|
|
4251
|
+
const dir = join3(runLogsDir, String(localRunId));
|
|
4186
4252
|
let files;
|
|
4187
4253
|
try {
|
|
4188
4254
|
files = readdirSync(dir);
|
|
@@ -4196,13 +4262,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
|
|
|
4196
4262
|
}
|
|
4197
4263
|
if (indexes.size === 0) return null;
|
|
4198
4264
|
const index = Math.max(...indexes);
|
|
4199
|
-
const out = [
|
|
4200
|
-
const err =
|
|
4265
|
+
const out = [join3(dir, `${index}.json`), join3(dir, `${index}.json.done`)].find((p) => existsSync3(p)) ?? null;
|
|
4266
|
+
const err = existsSync3(join3(dir, `${index}.err.log`)) ? join3(dir, `${index}.err.log`) : null;
|
|
4201
4267
|
return { index, total: indexes.size, out, err };
|
|
4202
4268
|
}
|
|
4203
4269
|
function runLogTail(runLogsDir, localRunId, opts) {
|
|
4204
4270
|
const launch = latestRunLaunch(runLogsDir, localRunId);
|
|
4205
|
-
if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${
|
|
4271
|
+
if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${join3(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
|
|
4206
4272
|
const parts = [];
|
|
4207
4273
|
let budget = opts.maxBytes;
|
|
4208
4274
|
if (launch.err) {
|
|
@@ -4336,6 +4402,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4336
4402
|
depsDoneInProd = /* @__PURE__ */ new Set();
|
|
4337
4403
|
depsAskedAt = /* @__PURE__ */ new Map();
|
|
4338
4404
|
static DEPS_TTL_MS = 6e4;
|
|
4405
|
+
// TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
|
|
4406
|
+
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
4407
|
+
// каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
|
|
4408
|
+
notifyLevelUnsupported = false;
|
|
4339
4409
|
// Корреляция с продом.
|
|
4340
4410
|
keyByLocal = /* @__PURE__ */ new Map();
|
|
4341
4411
|
// local run id → runKey
|
|
@@ -4375,7 +4445,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
4375
4445
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
4376
4446
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
4377
4447
|
this.host = opts.host ?? null;
|
|
4378
|
-
this.repoExists = opts.repoExists ??
|
|
4448
|
+
this.repoExists = opts.repoExists ?? existsSync4;
|
|
4379
4449
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
4380
4450
|
this.outboxPath = opts.outboxPath;
|
|
4381
4451
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
@@ -4385,15 +4455,15 @@ var SwarmClient = class _SwarmClient {
|
|
|
4385
4455
|
}
|
|
4386
4456
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
4387
4457
|
recover() {
|
|
4388
|
-
if (
|
|
4458
|
+
if (existsSync4(this.mapPath)) {
|
|
4389
4459
|
try {
|
|
4390
|
-
const obj = JSON.parse(
|
|
4460
|
+
const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
|
|
4391
4461
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
4392
4462
|
} catch {
|
|
4393
4463
|
}
|
|
4394
4464
|
}
|
|
4395
|
-
if (
|
|
4396
|
-
const txt =
|
|
4465
|
+
if (existsSync4(this.outboxPath)) {
|
|
4466
|
+
const txt = readFileSync4(this.outboxPath, "utf8");
|
|
4397
4467
|
for (const line of txt.split("\n")) {
|
|
4398
4468
|
const s = line.trim();
|
|
4399
4469
|
if (!s) continue;
|
|
@@ -4413,10 +4483,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4413
4483
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
4414
4484
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
4415
4485
|
replayGlobalEvents() {
|
|
4416
|
-
if (!
|
|
4486
|
+
if (!existsSync4(this.stormPath)) return;
|
|
4417
4487
|
let txt;
|
|
4418
4488
|
try {
|
|
4419
|
-
txt =
|
|
4489
|
+
txt = readFileSync4(this.stormPath, "utf8");
|
|
4420
4490
|
} catch {
|
|
4421
4491
|
return;
|
|
4422
4492
|
}
|
|
@@ -5084,7 +5154,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
5084
5154
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
5085
5155
|
*/
|
|
5086
5156
|
reconcileRuntime(opts) {
|
|
5087
|
-
const wtExists = opts.worktreeExists ??
|
|
5157
|
+
const wtExists = opts.worktreeExists ?? existsSync4;
|
|
5088
5158
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
5089
5159
|
try {
|
|
5090
5160
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -5397,9 +5467,32 @@ var SwarmClient = class _SwarmClient {
|
|
|
5397
5467
|
lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
|
|
5398
5468
|
// TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
|
|
5399
5469
|
// Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
|
|
5400
|
-
reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : []
|
|
5470
|
+
reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
|
|
5471
|
+
// TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
|
|
5472
|
+
// версии поля не пришлёт → undefined, строка выглядит как раньше.
|
|
5473
|
+
...typeof h.paused === "boolean" ? { paused: h.paused } : {},
|
|
5474
|
+
...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
|
|
5475
|
+
...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {}
|
|
5401
5476
|
}));
|
|
5402
5477
|
}
|
|
5478
|
+
/**
|
|
5479
|
+
* TASWM-326: убрать ЭТУ машину из парка (`swarm remove`). Прод удаляет строку
|
|
5480
|
+
* SwarmHost и освобождает её незавершённые прогоны — ровно то же, что делает свипер
|
|
5481
|
+
* с молчащей машиной, только сразу и по явной команде. Без этого выведенная из роя
|
|
5482
|
+
* машина висела в списке вечно с подписью «молчит N минут», неотличимо от упавшей.
|
|
5483
|
+
*
|
|
5484
|
+
* ПРЯМОЙ вызов, не outbox: это не durable-факт роя, который надо досылать после
|
|
5485
|
+
* восстановления сети, — команда имеет смысл только сейчас. Бросает при сбое:
|
|
5486
|
+
* вызывающий обязан сказать человеку, что из парка машину убрать не удалось, а не
|
|
5487
|
+
* молча стереть локальные токены (тогда её вообще нечем будет убрать).
|
|
5488
|
+
*/
|
|
5489
|
+
async forgetHost() {
|
|
5490
|
+
if (!this.host) throw new Error("\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u043B\u0430\u0441\u044C (\u043D\u0435\u0442 hostId) \u2014 \u043D\u0435\u0447\u0435\u0433\u043E \u0443\u0431\u0438\u0440\u0430\u0442\u044C \u0438\u0437 \u043F\u0430\u0440\u043A\u0430");
|
|
5491
|
+
const res = await this.call("swarm_host_forget", { hostId: this.host.hostId });
|
|
5492
|
+
if (res?.isError) throw new Error("swarm_host_forget \u043E\u0442\u0432\u0435\u0440\u0433 \u0437\u0430\u043F\u0440\u043E\u0441 \u2014 \u043C\u0430\u0448\u0438\u043D\u0430 \u043E\u0441\u0442\u0430\u043B\u0430\u0441\u044C \u0432 \u043F\u0430\u0440\u043A\u0435");
|
|
5493
|
+
const sc = res.structuredContent ?? {};
|
|
5494
|
+
return { removed: sc.removed !== false, released: typeof sc.released === "number" ? sc.released : 0 };
|
|
5495
|
+
}
|
|
5403
5496
|
/**
|
|
5404
5497
|
* Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
|
|
5405
5498
|
* дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
|
|
@@ -5427,10 +5520,21 @@ var SwarmClient = class _SwarmClient {
|
|
|
5427
5520
|
* «пришли хвост лога» (logRequests). Возвращаем их вызывающему; пульс недоступен или
|
|
5428
5521
|
* прод старый (поля нет) → пустой список, всё как раньше.
|
|
5429
5522
|
*/
|
|
5430
|
-
|
|
5523
|
+
/**
|
|
5524
|
+
* TASWM-326: тем же пульсом уходит ПАУЗА этой машины (`pause` — durable-запись из
|
|
5525
|
+
* pause.json либо null). В парке машин «на паузе» обязано отличаться от «нет работы»
|
|
5526
|
+
* и от «молчит», а единственный канал «демон → прод» с нужной кадансностью — пульс.
|
|
5527
|
+
* Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
|
|
5528
|
+
* означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
|
|
5529
|
+
*/
|
|
5530
|
+
async heartbeat(daemonVersion, pause) {
|
|
5431
5531
|
try {
|
|
5432
5532
|
const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
|
|
5433
5533
|
if (daemonVersion) args.daemonVersion = daemonVersion;
|
|
5534
|
+
if (pause !== void 0) {
|
|
5535
|
+
args.paused = !!pause?.paused;
|
|
5536
|
+
if (pause?.reason) args.pauseReason = pause.reason;
|
|
5537
|
+
}
|
|
5434
5538
|
if (this.host) {
|
|
5435
5539
|
args.hostId = this.host.hostId;
|
|
5436
5540
|
args.hostName = this.host.hostName;
|
|
@@ -5486,13 +5590,26 @@ var SwarmClient = class _SwarmClient {
|
|
|
5486
5590
|
*/
|
|
5487
5591
|
async notifyAnomaly(signal) {
|
|
5488
5592
|
const prodRunId = signal.runId != null ? this.prodIdByKey.get(this.runKeyFor(signal.runId) ?? "") : void 0;
|
|
5489
|
-
const
|
|
5593
|
+
const base3 = {
|
|
5490
5594
|
tag: signal.key,
|
|
5491
5595
|
title: signal.title,
|
|
5492
5596
|
body: signal.body,
|
|
5493
5597
|
...signal.href ? { href: signal.href } : {},
|
|
5494
5598
|
...prodRunId ? { runId: prodRunId } : {}
|
|
5495
|
-
}
|
|
5599
|
+
};
|
|
5600
|
+
if (!this.notifyLevelUnsupported) {
|
|
5601
|
+
const res2 = await this.call("swarm_notify", { ...base3, level: signal.level ?? "decision" });
|
|
5602
|
+
if (!res2?.isError) return;
|
|
5603
|
+
const text = JSON.stringify(res2);
|
|
5604
|
+
if (!(text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D") && text.includes("\u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442") && text.includes("level"))) {
|
|
5605
|
+
throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
5606
|
+
}
|
|
5607
|
+
this.notifyLevelUnsupported = true;
|
|
5608
|
+
console.warn(
|
|
5609
|
+
"[swarm-client] \u043F\u0440\u043E\u0434 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 (swarm_notify \u0431\u0435\u0437 level) \u2014 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F \u0440\u043E\u044F \u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u0438\u0434\u0443\u0442 \u043E\u0434\u043D\u0438\u043C \u0443\u0440\u043E\u0432\u043D\u0435\u043C, \u043A\u0430\u043A \u0434\u043E TASWM-327. \u041E\u0431\u043D\u043E\u0432\u0438 \u043F\u0440\u043E\u0434 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0434\u0435\u043C\u043E\u043D."
|
|
5610
|
+
);
|
|
5611
|
+
}
|
|
5612
|
+
const res = await this.call("swarm_notify", base3);
|
|
5496
5613
|
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
5497
5614
|
}
|
|
5498
5615
|
/**
|
|
@@ -5863,13 +5980,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
5863
5980
|
}
|
|
5864
5981
|
persistPending() {
|
|
5865
5982
|
try {
|
|
5866
|
-
|
|
5983
|
+
writeFileSync3(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
5867
5984
|
} catch {
|
|
5868
5985
|
}
|
|
5869
5986
|
}
|
|
5870
5987
|
persistMap() {
|
|
5871
5988
|
try {
|
|
5872
|
-
|
|
5989
|
+
writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
5873
5990
|
} catch {
|
|
5874
5991
|
}
|
|
5875
5992
|
}
|
|
@@ -5925,18 +6042,18 @@ async function buildRepoRegistry(paths) {
|
|
|
5925
6042
|
|
|
5926
6043
|
// src/hostIdentity.ts
|
|
5927
6044
|
import { hostname } from "os";
|
|
5928
|
-
import { existsSync as
|
|
5929
|
-
import { join as
|
|
6045
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
6046
|
+
import { join as join4 } from "path";
|
|
5930
6047
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5931
6048
|
function hostIdentityPath(home) {
|
|
5932
|
-
return
|
|
6049
|
+
return join4(home, "host.json");
|
|
5933
6050
|
}
|
|
5934
6051
|
function loadHostIdentity(deps) {
|
|
5935
|
-
const exists = deps.exists ??
|
|
5936
|
-
const readFile2 = deps.readFile ?? ((p) =>
|
|
6052
|
+
const exists = deps.exists ?? existsSync5;
|
|
6053
|
+
const readFile2 = deps.readFile ?? ((p) => readFileSync6(p, "utf8"));
|
|
5937
6054
|
const writeFile2 = deps.writeFile ?? ((p, data) => {
|
|
5938
|
-
|
|
5939
|
-
|
|
6055
|
+
writeFileSync5(`${p}.tmp`, data);
|
|
6056
|
+
renameSync4(`${p}.tmp`, p);
|
|
5940
6057
|
});
|
|
5941
6058
|
const host = deps.hostname ?? hostname;
|
|
5942
6059
|
const newId = deps.newId ?? randomUUID2;
|
|
@@ -6118,14 +6235,14 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
|
6118
6235
|
|
|
6119
6236
|
// src/launcher.ts
|
|
6120
6237
|
import { spawn } from "child_process";
|
|
6121
|
-
import { closeSync as closeSync2, existsSync as
|
|
6238
|
+
import { closeSync as closeSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6122
6239
|
import { createRequire as createRequire2 } from "module";
|
|
6123
6240
|
import { homedir as homedir2 } from "os";
|
|
6124
|
-
import { dirname as dirname2, join as
|
|
6241
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
6125
6242
|
import { fileURLToPath } from "url";
|
|
6126
6243
|
|
|
6127
6244
|
// src/claudeBin.ts
|
|
6128
|
-
import { existsSync as
|
|
6245
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
|
|
6129
6246
|
import { win32 as winPath } from "path";
|
|
6130
6247
|
var CLAUDE_FALLBACK = "claude";
|
|
6131
6248
|
function compareVersionDirs(a, b) {
|
|
@@ -6162,7 +6279,7 @@ function winVersionRoots(env, listDir) {
|
|
|
6162
6279
|
function findWindowsClaude(opts = {}) {
|
|
6163
6280
|
const env = opts.env ?? process.env;
|
|
6164
6281
|
const listDir = opts.listDir ?? realListDir;
|
|
6165
|
-
const fileExists = opts.fileExists ??
|
|
6282
|
+
const fileExists = opts.fileExists ?? existsSync6;
|
|
6166
6283
|
let best = null;
|
|
6167
6284
|
for (const root of winVersionRoots(env, listDir)) {
|
|
6168
6285
|
for (const version of listDir(root)) {
|
|
@@ -6198,7 +6315,7 @@ function resolveClaudeBin(opts = {}) {
|
|
|
6198
6315
|
|
|
6199
6316
|
// src/droneHome.ts
|
|
6200
6317
|
import { lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync, statSync as statSync2, symlinkSync } from "fs";
|
|
6201
|
-
import { join as
|
|
6318
|
+
import { join as join5, relative, sep } from "path";
|
|
6202
6319
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
6203
6320
|
function droneHomeDeny(env = process.env) {
|
|
6204
6321
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -6222,20 +6339,20 @@ function buildDroneHome(opts) {
|
|
|
6222
6339
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
6223
6340
|
for (const existing of readdirSync3(droneHome)) {
|
|
6224
6341
|
if (wanted.has(existing)) continue;
|
|
6225
|
-
const p =
|
|
6342
|
+
const p = join5(droneHome, existing);
|
|
6226
6343
|
try {
|
|
6227
6344
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
6228
6345
|
} catch {
|
|
6229
6346
|
}
|
|
6230
6347
|
}
|
|
6231
6348
|
for (const name of wanted) {
|
|
6232
|
-
const link =
|
|
6349
|
+
const link = join5(droneHome, name);
|
|
6233
6350
|
try {
|
|
6234
6351
|
lstatSync(link);
|
|
6235
6352
|
continue;
|
|
6236
6353
|
} catch {
|
|
6237
6354
|
}
|
|
6238
|
-
const target =
|
|
6355
|
+
const target = join5(realHome, name);
|
|
6239
6356
|
let isDir;
|
|
6240
6357
|
try {
|
|
6241
6358
|
isDir = statSync2(target).isDirectory();
|
|
@@ -6256,7 +6373,7 @@ var REAL_PATH_ENV_PINS = [
|
|
|
6256
6373
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
6257
6374
|
];
|
|
6258
6375
|
function realPathEnvPins(realHome) {
|
|
6259
|
-
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env,
|
|
6376
|
+
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join5(realHome, ...segments)]));
|
|
6260
6377
|
}
|
|
6261
6378
|
function droneDenyRules(realHome, swarmHome) {
|
|
6262
6379
|
const paths = [
|
|
@@ -6348,7 +6465,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
|
6348
6465
|
const env = { ...base3 };
|
|
6349
6466
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
6350
6467
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
6351
|
-
env.TURBO_CACHE_DIR =
|
|
6468
|
+
env.TURBO_CACHE_DIR = join6(worktree, ".turbo", "cache");
|
|
6352
6469
|
}
|
|
6353
6470
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
6354
6471
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -6376,25 +6493,25 @@ var DAEMON_STATE_ENV = [
|
|
|
6376
6493
|
"TASKLESS_TOKEN"
|
|
6377
6494
|
];
|
|
6378
6495
|
function bundledRoleDir(kind) {
|
|
6379
|
-
return
|
|
6496
|
+
return join6(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
|
|
6380
6497
|
}
|
|
6381
6498
|
function roleDir(worktree, kind) {
|
|
6382
|
-
return
|
|
6499
|
+
return join6(worktree, ".claude", "roles", kind);
|
|
6383
6500
|
}
|
|
6384
6501
|
function roleDirs(worktree, kind) {
|
|
6385
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
6502
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
|
|
6386
6503
|
}
|
|
6387
6504
|
function transcriptDir(worktree, projectsDir) {
|
|
6388
|
-
return
|
|
6505
|
+
return join6(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
6389
6506
|
}
|
|
6390
|
-
function sessionTranscriptExists(worktree, projectsDir =
|
|
6507
|
+
function sessionTranscriptExists(worktree, projectsDir = join6(homedir2(), ".claude", "projects")) {
|
|
6391
6508
|
try {
|
|
6392
6509
|
return readdirSync4(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
6393
6510
|
} catch {
|
|
6394
6511
|
return false;
|
|
6395
6512
|
}
|
|
6396
6513
|
}
|
|
6397
|
-
function transcriptMtimeMs(worktree, projectsDir =
|
|
6514
|
+
function transcriptMtimeMs(worktree, projectsDir = join6(homedir2(), ".claude", "projects")) {
|
|
6398
6515
|
const dir = transcriptDir(worktree, projectsDir);
|
|
6399
6516
|
let files;
|
|
6400
6517
|
try {
|
|
@@ -6406,7 +6523,7 @@ function transcriptMtimeMs(worktree, projectsDir = join5(homedir2(), ".claude",
|
|
|
6406
6523
|
for (const f of files) {
|
|
6407
6524
|
if (!f.endsWith(".jsonl")) continue;
|
|
6408
6525
|
try {
|
|
6409
|
-
const mtime2 = statSync3(
|
|
6526
|
+
const mtime2 = statSync3(join6(dir, f)).mtimeMs;
|
|
6410
6527
|
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
6411
6528
|
} catch {
|
|
6412
6529
|
}
|
|
@@ -6414,7 +6531,7 @@ function transcriptMtimeMs(worktree, projectsDir = join5(homedir2(), ".claude",
|
|
|
6414
6531
|
return latest;
|
|
6415
6532
|
}
|
|
6416
6533
|
function nextRunLogPath(runLogsDir, runId) {
|
|
6417
|
-
const dir =
|
|
6534
|
+
const dir = join6(runLogsDir, String(runId));
|
|
6418
6535
|
mkdirSync4(dir, { recursive: true });
|
|
6419
6536
|
let files = [];
|
|
6420
6537
|
try {
|
|
@@ -6426,7 +6543,7 @@ function nextRunLogPath(runLogsDir, runId) {
|
|
|
6426
6543
|
const m = /^(\d+)\.json/.exec(f);
|
|
6427
6544
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
6428
6545
|
}
|
|
6429
|
-
return
|
|
6546
|
+
return join6(dir, `${next}.json`);
|
|
6430
6547
|
}
|
|
6431
6548
|
function openNextRunLog(runLogsDir, runId) {
|
|
6432
6549
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -6500,16 +6617,16 @@ function makeLauncher(cfg) {
|
|
|
6500
6617
|
}
|
|
6501
6618
|
}
|
|
6502
6619
|
};
|
|
6503
|
-
const cfgPath =
|
|
6504
|
-
|
|
6620
|
+
const cfgPath = join6(worktree, ".swarm-mcp.json");
|
|
6621
|
+
writeFileSync6(cfgPath, JSON.stringify(mcpConfig, null, 2));
|
|
6505
6622
|
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
|
|
6506
6623
|
const pluginDirs = roleDirs(worktree, run.kind);
|
|
6507
6624
|
const realHome = cfg.droneIsolation?.realHome ?? homedir2();
|
|
6508
6625
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
6509
6626
|
const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
|
|
6510
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !
|
|
6627
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join6(cfg.droneIsolation.swarmHome, "token"))) {
|
|
6511
6628
|
console.error(
|
|
6512
|
-
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${
|
|
6629
|
+
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join6(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
|
|
6513
6630
|
);
|
|
6514
6631
|
}
|
|
6515
6632
|
let stdio = "ignore";
|
|
@@ -6543,8 +6660,8 @@ function makeLauncher(cfg) {
|
|
|
6543
6660
|
init_procCleanup();
|
|
6544
6661
|
|
|
6545
6662
|
// src/cost.ts
|
|
6546
|
-
import { readFileSync as
|
|
6547
|
-
import { join as
|
|
6663
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync5, renameSync as renameSync5 } from "fs";
|
|
6664
|
+
import { join as join7 } from "path";
|
|
6548
6665
|
|
|
6549
6666
|
// src/pricing.ts
|
|
6550
6667
|
var MODEL_PRICING = {
|
|
@@ -6601,17 +6718,17 @@ function unprocessedLogFiles(dir) {
|
|
|
6601
6718
|
}
|
|
6602
6719
|
}
|
|
6603
6720
|
function accountRunCost(deps, run) {
|
|
6604
|
-
const dir =
|
|
6721
|
+
const dir = join7(deps.runLogsDir, String(run.id));
|
|
6605
6722
|
const files = unprocessedLogFiles(dir);
|
|
6606
6723
|
if (files.length === 0) return;
|
|
6607
6724
|
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
6608
6725
|
let added = 0;
|
|
6609
6726
|
const processed = [];
|
|
6610
6727
|
for (const f of files) {
|
|
6611
|
-
const full =
|
|
6728
|
+
const full = join7(dir, f);
|
|
6612
6729
|
let raw = null;
|
|
6613
6730
|
try {
|
|
6614
|
-
raw =
|
|
6731
|
+
raw = readFileSync7(full, "utf8");
|
|
6615
6732
|
} catch {
|
|
6616
6733
|
continue;
|
|
6617
6734
|
}
|
|
@@ -6628,7 +6745,7 @@ function accountRunCost(deps, run) {
|
|
|
6628
6745
|
if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
|
|
6629
6746
|
for (const full of processed) {
|
|
6630
6747
|
try {
|
|
6631
|
-
|
|
6748
|
+
renameSync5(full, `${full}.done`);
|
|
6632
6749
|
} catch {
|
|
6633
6750
|
}
|
|
6634
6751
|
}
|
|
@@ -6638,11 +6755,11 @@ function makeCostAccountant(deps) {
|
|
|
6638
6755
|
}
|
|
6639
6756
|
|
|
6640
6757
|
// src/runOutcome.ts
|
|
6641
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
6642
|
-
import { join as
|
|
6758
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
|
|
6759
|
+
import { join as join8 } from "path";
|
|
6643
6760
|
var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
|
|
6644
6761
|
function classifyExit(runLogsDir, runId) {
|
|
6645
|
-
const dir =
|
|
6762
|
+
const dir = join8(runLogsDir, String(runId));
|
|
6646
6763
|
let files;
|
|
6647
6764
|
try {
|
|
6648
6765
|
files = readdirSync6(dir);
|
|
@@ -6659,7 +6776,7 @@ function classifyExit(runLogsDir, runId) {
|
|
|
6659
6776
|
if (!latest) return "unknown";
|
|
6660
6777
|
let raw;
|
|
6661
6778
|
try {
|
|
6662
|
-
raw =
|
|
6779
|
+
raw = readFileSync8(join8(dir, latest.name), "utf8");
|
|
6663
6780
|
} catch {
|
|
6664
6781
|
return "unknown";
|
|
6665
6782
|
}
|
|
@@ -6673,8 +6790,8 @@ function makeExitClassifier(runLogsDir) {
|
|
|
6673
6790
|
init_git();
|
|
6674
6791
|
|
|
6675
6792
|
// src/deps.ts
|
|
6676
|
-
import { existsSync as
|
|
6677
|
-
import { join as
|
|
6793
|
+
import { existsSync as existsSync9, readdirSync as readdirSync8, readFileSync as readFileSync9 } from "fs";
|
|
6794
|
+
import { join as join10 } from "path";
|
|
6678
6795
|
import { execa } from "execa";
|
|
6679
6796
|
var PREPARE_TIMEOUT_MS = 15 * 6e4;
|
|
6680
6797
|
var MAX_PLAN_COMMANDS = 12;
|
|
@@ -6690,7 +6807,7 @@ async function runCommand(cmd, worktree) {
|
|
|
6690
6807
|
}
|
|
6691
6808
|
function safeReadText(p) {
|
|
6692
6809
|
try {
|
|
6693
|
-
return
|
|
6810
|
+
return readFileSync9(p, "utf8");
|
|
6694
6811
|
} catch {
|
|
6695
6812
|
return null;
|
|
6696
6813
|
}
|
|
@@ -6730,21 +6847,21 @@ function overridePlan(override) {
|
|
|
6730
6847
|
};
|
|
6731
6848
|
}
|
|
6732
6849
|
function detectPackageManager(worktree, io = {}) {
|
|
6733
|
-
const exists = io.exists ??
|
|
6850
|
+
const exists = io.exists ?? existsSync9;
|
|
6734
6851
|
const readText = io.readText ?? safeReadText;
|
|
6735
|
-
const hasPkgJson = exists(
|
|
6852
|
+
const hasPkgJson = exists(join10(worktree, "package.json"));
|
|
6736
6853
|
if (!hasPkgJson) {
|
|
6737
|
-
if (exists(
|
|
6854
|
+
if (exists(join10(worktree, "deno.json")) || exists(join10(worktree, "deno.jsonc")) || exists(join10(worktree, "deno.lock"))) return "deno";
|
|
6738
6855
|
return null;
|
|
6739
6856
|
}
|
|
6740
|
-
const pkg = readJson(readText,
|
|
6857
|
+
const pkg = readJson(readText, join10(worktree, "package.json"));
|
|
6741
6858
|
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
|
|
6742
6859
|
if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
|
|
6743
|
-
if (exists(
|
|
6744
|
-
if (exists(
|
|
6745
|
-
if (exists(
|
|
6746
|
-
if (exists(
|
|
6747
|
-
if (exists(
|
|
6860
|
+
if (exists(join10(worktree, "bun.lock")) || exists(join10(worktree, "bun.lockb"))) return "bun";
|
|
6861
|
+
if (exists(join10(worktree, "pnpm-lock.yaml"))) return "pnpm";
|
|
6862
|
+
if (exists(join10(worktree, "yarn.lock"))) return "yarn";
|
|
6863
|
+
if (exists(join10(worktree, "package-lock.json"))) return "npm";
|
|
6864
|
+
if (exists(join10(worktree, "deno.lock"))) return "deno";
|
|
6748
6865
|
return "npm";
|
|
6749
6866
|
}
|
|
6750
6867
|
function jsInstallCommand(pm) {
|
|
@@ -6797,7 +6914,7 @@ var ECOSYSTEM_MARKERS = [
|
|
|
6797
6914
|
// конфигурация — дело сборки, не подготовки
|
|
6798
6915
|
];
|
|
6799
6916
|
function detectPlan(worktree, io) {
|
|
6800
|
-
const exists = io.exists ??
|
|
6917
|
+
const exists = io.exists ?? existsSync9;
|
|
6801
6918
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
6802
6919
|
const pm = detectPackageManager(worktree, io);
|
|
6803
6920
|
if (pm) {
|
|
@@ -6805,7 +6922,7 @@ function detectPlan(worktree, io) {
|
|
|
6805
6922
|
// node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
|
|
6806
6923
|
// У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
|
|
6807
6924
|
// заново, но он дешёвый: тянет из того же общего кэша.
|
|
6808
|
-
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists:
|
|
6925
|
+
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join10(worktree, "node_modules"), source: "detect" }
|
|
6809
6926
|
];
|
|
6810
6927
|
for (const target of workspaceBuildTargets(worktree, io)) {
|
|
6811
6928
|
commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
|
|
@@ -6813,12 +6930,12 @@ function detectPlan(worktree, io) {
|
|
|
6813
6930
|
return { source: "detect", ecosystem: `node/${pm}`, commands };
|
|
6814
6931
|
}
|
|
6815
6932
|
for (const marker of ECOSYSTEM_MARKERS) {
|
|
6816
|
-
const hit = marker.files.some((f) => exists(
|
|
6933
|
+
const hit = marker.files.some((f) => exists(join10(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
|
|
6817
6934
|
if (!hit) continue;
|
|
6818
6935
|
let ecosystem = marker.ecosystem;
|
|
6819
6936
|
let command = marker.command;
|
|
6820
6937
|
for (const refine of marker.refine ?? []) {
|
|
6821
|
-
if (!exists(
|
|
6938
|
+
if (!exists(join10(worktree, refine.file))) continue;
|
|
6822
6939
|
ecosystem = refine.ecosystem;
|
|
6823
6940
|
command = refine.command;
|
|
6824
6941
|
break;
|
|
@@ -6875,12 +6992,12 @@ var CI_INSTALL_PATTERNS = [
|
|
|
6875
6992
|
function ciWorkflowPlan(worktree, io) {
|
|
6876
6993
|
const readText = io.readText ?? safeReadText;
|
|
6877
6994
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
6878
|
-
const dir =
|
|
6995
|
+
const dir = join10(worktree, ".github", "workflows");
|
|
6879
6996
|
const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
|
|
6880
6997
|
const seen = /* @__PURE__ */ new Set();
|
|
6881
6998
|
const commands = [];
|
|
6882
6999
|
for (const name of files.sort()) {
|
|
6883
|
-
const yaml = readText(
|
|
7000
|
+
const yaml = readText(join10(dir, name));
|
|
6884
7001
|
if (yaml === null) continue;
|
|
6885
7002
|
for (const raw of workflowRunLines(yaml)) {
|
|
6886
7003
|
const line = raw.replace(/\s+/g, " ").trim();
|
|
@@ -6895,7 +7012,7 @@ function ciWorkflowPlan(worktree, io) {
|
|
|
6895
7012
|
return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
|
|
6896
7013
|
}
|
|
6897
7014
|
function workspaceGlobs(worktree, readText) {
|
|
6898
|
-
const yaml = readText(
|
|
7015
|
+
const yaml = readText(join10(worktree, "pnpm-workspace.yaml"));
|
|
6899
7016
|
if (yaml !== null) {
|
|
6900
7017
|
const globs = [];
|
|
6901
7018
|
let inPackages = false;
|
|
@@ -6915,7 +7032,7 @@ function workspaceGlobs(worktree, readText) {
|
|
|
6915
7032
|
}
|
|
6916
7033
|
if (globs.length) return globs;
|
|
6917
7034
|
}
|
|
6918
|
-
const pkg = readJson(readText,
|
|
7035
|
+
const pkg = readJson(readText, join10(worktree, "package.json"));
|
|
6919
7036
|
const ws = pkg?.workspaces;
|
|
6920
7037
|
const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
6921
7038
|
return list.filter((g) => typeof g === "string");
|
|
@@ -6926,14 +7043,14 @@ function workspaceDirs(worktree, globs, io) {
|
|
|
6926
7043
|
if (glob.startsWith("!")) continue;
|
|
6927
7044
|
const star = glob.indexOf("*");
|
|
6928
7045
|
if (star === -1) {
|
|
6929
|
-
const dir =
|
|
6930
|
-
if (io.exists(
|
|
7046
|
+
const dir = join10(worktree, glob);
|
|
7047
|
+
if (io.exists(join10(dir, "package.json"))) dirs.add(dir);
|
|
6931
7048
|
continue;
|
|
6932
7049
|
}
|
|
6933
|
-
const base3 =
|
|
7050
|
+
const base3 = join10(worktree, glob.slice(0, star).replace(/\/$/, ""));
|
|
6934
7051
|
for (const name of io.listDirs(base3)) {
|
|
6935
|
-
const dir =
|
|
6936
|
-
if (io.exists(
|
|
7052
|
+
const dir = join10(base3, name);
|
|
7053
|
+
if (io.exists(join10(dir, "package.json"))) dirs.add(dir);
|
|
6937
7054
|
}
|
|
6938
7055
|
}
|
|
6939
7056
|
return [...dirs];
|
|
@@ -6954,18 +7071,18 @@ function entryPaths(pkg) {
|
|
|
6954
7071
|
return out;
|
|
6955
7072
|
}
|
|
6956
7073
|
function workspaceBuildTargets(worktree, io = {}) {
|
|
6957
|
-
const exists = io.exists ??
|
|
7074
|
+
const exists = io.exists ?? existsSync9;
|
|
6958
7075
|
const readText = io.readText ?? safeReadText;
|
|
6959
7076
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
6960
7077
|
const globs = workspaceGlobs(worktree, readText);
|
|
6961
7078
|
if (!globs.length) return [];
|
|
6962
7079
|
const pkgs = [];
|
|
6963
7080
|
for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
|
|
6964
|
-
const pkg = readJson(readText,
|
|
7081
|
+
const pkg = readJson(readText, join10(dir, "package.json"));
|
|
6965
7082
|
if (!pkg || typeof pkg.name !== "string") continue;
|
|
6966
7083
|
const scripts = pkg.scripts ?? {};
|
|
6967
7084
|
const entries = entryPaths(pkg);
|
|
6968
|
-
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(
|
|
7085
|
+
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join10(dir, e)));
|
|
6969
7086
|
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
6970
7087
|
pkgs.push({ name: pkg.name, dir, deps, needsBuild });
|
|
6971
7088
|
}
|
|
@@ -6993,7 +7110,7 @@ function planPrepare(worktree, io = {}) {
|
|
|
6993
7110
|
return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
|
|
6994
7111
|
}
|
|
6995
7112
|
async function ensureWorktreeDeps(worktree, io = {}) {
|
|
6996
|
-
const exists = io.exists ??
|
|
7113
|
+
const exists = io.exists ?? existsSync9;
|
|
6997
7114
|
const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
|
|
6998
7115
|
const approve = io.approve ?? (() => true);
|
|
6999
7116
|
const plan = planPrepare(worktree, io);
|
|
@@ -7892,13 +8009,74 @@ import { createHash as createHash2 } from "crypto";
|
|
|
7892
8009
|
|
|
7893
8010
|
// src/anomalies.ts
|
|
7894
8011
|
init_src();
|
|
8012
|
+
var LEVEL_RANK = { note: 0, feed: 1, decision: 2 };
|
|
8013
|
+
function plural2(n, forms) {
|
|
8014
|
+
const mod100 = n % 100;
|
|
8015
|
+
if (mod100 >= 11 && mod100 <= 14) return forms[2];
|
|
8016
|
+
const mod10 = n % 10;
|
|
8017
|
+
if (mod10 === 1) return forms[0];
|
|
8018
|
+
if (mod10 >= 2 && mod10 <= 4) return forms[1];
|
|
8019
|
+
return forms[2];
|
|
8020
|
+
}
|
|
8021
|
+
function collapseSignals(candidates) {
|
|
8022
|
+
const single = (c) => {
|
|
8023
|
+
const signal = { key: c.key, ...c.build() };
|
|
8024
|
+
return { signal, keys: [c.key], events: signal.runId != null ? [{ runId: signal.runId, text: signal.body }] : [] };
|
|
8025
|
+
};
|
|
8026
|
+
const out = [];
|
|
8027
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
8028
|
+
for (const c of candidates) {
|
|
8029
|
+
if (!c.group) {
|
|
8030
|
+
out.push(single(c));
|
|
8031
|
+
continue;
|
|
8032
|
+
}
|
|
8033
|
+
const bucket = grouped.get(c.group.id);
|
|
8034
|
+
if (bucket) bucket.members.push(c);
|
|
8035
|
+
else grouped.set(c.group.id, { group: c.group, members: [c] });
|
|
8036
|
+
}
|
|
8037
|
+
for (const { group, members } of grouped.values()) {
|
|
8038
|
+
const first = members[0];
|
|
8039
|
+
if (members.length === 1 && first) {
|
|
8040
|
+
out.push(single(first));
|
|
8041
|
+
continue;
|
|
8042
|
+
}
|
|
8043
|
+
const built = members.map((m) => ({ key: m.key, signal: m.build() }));
|
|
8044
|
+
const level = built.reduce(
|
|
8045
|
+
(acc, b) => LEVEL_RANK[b.signal.level ?? "decision"] > LEVEL_RANK[acc] ? b.signal.level ?? "decision" : acc,
|
|
8046
|
+
"note"
|
|
8047
|
+
);
|
|
8048
|
+
out.push({
|
|
8049
|
+
signal: {
|
|
8050
|
+
key: `${group.id}_group`,
|
|
8051
|
+
title: group.title(built.length),
|
|
8052
|
+
body: joinCapped(built.map((b) => b.signal.title)),
|
|
8053
|
+
level
|
|
8054
|
+
},
|
|
8055
|
+
keys: built.map((b) => b.key),
|
|
8056
|
+
events: built.flatMap((b) => b.signal.runId != null ? [{ runId: b.signal.runId, text: b.signal.body }] : [])
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
return out;
|
|
8060
|
+
}
|
|
8061
|
+
var LAST_REMINDER_SUFFIX = "\u041D\u0430\u043F\u043E\u043C\u0438\u043D\u0430\u044E \u043E\u0431 \u044D\u0442\u043E\u043C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0440\u0430\u0437 \u2014 \u0440\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0438\u043B\u0438 \u0441\u043D\u0438\u043C\u0438 \u043F\u0440\u043E\u0433\u043E\u043D, \u0434\u0430\u043B\u044C\u0448\u0435 \u0431\u0443\u0434\u0443 \u043C\u043E\u043B\u0447\u0430\u0442\u044C.";
|
|
7895
8062
|
var AnomalyTracker = class {
|
|
7896
|
-
constructor(renotifyCooldownMs) {
|
|
8063
|
+
constructor(renotifyCooldownMs, opts = {}) {
|
|
7897
8064
|
this.renotifyCooldownMs = renotifyCooldownMs;
|
|
8065
|
+
this.maxRepeats = opts.maxRepeats && opts.maxRepeats > 0 ? Math.trunc(opts.maxRepeats) : Number.POSITIVE_INFINITY;
|
|
8066
|
+
this.io = opts.io;
|
|
8067
|
+
const restored = safeLoad(this.io);
|
|
8068
|
+
if (restored) {
|
|
8069
|
+
for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
|
|
8070
|
+
for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
|
|
8071
|
+
for (const [k, v] of Object.entries(restored.fires)) this.fires.set(k, v);
|
|
8072
|
+
}
|
|
7898
8073
|
}
|
|
7899
8074
|
renotifyCooldownMs;
|
|
7900
8075
|
since = /* @__PURE__ */ new Map();
|
|
7901
8076
|
lastFiredAt = /* @__PURE__ */ new Map();
|
|
8077
|
+
fires = /* @__PURE__ */ new Map();
|
|
8078
|
+
maxRepeats;
|
|
8079
|
+
io;
|
|
7902
8080
|
/** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
|
|
7903
8081
|
eligible(now, candidates) {
|
|
7904
8082
|
const result = [];
|
|
@@ -7910,17 +8088,58 @@ var AnomalyTracker = class {
|
|
|
7910
8088
|
if (now - startedAt < c.minDurationMs) continue;
|
|
7911
8089
|
const lastFired = this.lastFiredAt.get(c.key);
|
|
7912
8090
|
if (lastFired != null && now - lastFired < this.renotifyCooldownMs) continue;
|
|
8091
|
+
if ((this.fires.get(c.key) ?? 0) >= this.maxRepeats) continue;
|
|
7913
8092
|
result.push(c);
|
|
7914
8093
|
}
|
|
7915
|
-
|
|
7916
|
-
for (const
|
|
8094
|
+
let pruned = false;
|
|
8095
|
+
for (const map of [this.since, this.lastFiredAt, this.fires]) {
|
|
8096
|
+
for (const k of [...map.keys()]) {
|
|
8097
|
+
if (!seen.has(k)) {
|
|
8098
|
+
map.delete(k);
|
|
8099
|
+
pruned = true;
|
|
8100
|
+
}
|
|
8101
|
+
}
|
|
8102
|
+
}
|
|
8103
|
+
if (pruned) this.persist();
|
|
7917
8104
|
return result;
|
|
7918
8105
|
}
|
|
7919
8106
|
/** Доставка signal с этим key успешно ушла в этот момент — начинает кулдаун повтора. */
|
|
7920
8107
|
commit(key, now) {
|
|
7921
8108
|
this.lastFiredAt.set(key, now);
|
|
8109
|
+
this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
|
|
8110
|
+
this.persist();
|
|
8111
|
+
}
|
|
8112
|
+
/**
|
|
8113
|
+
* Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
|
|
8114
|
+
* формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
|
|
8115
|
+
* Без потолка (maxRepeats не задан) — всегда false.
|
|
8116
|
+
*/
|
|
8117
|
+
isFinalRepeat(key) {
|
|
8118
|
+
if (!Number.isFinite(this.maxRepeats)) return false;
|
|
8119
|
+
return (this.fires.get(key) ?? 0) === this.maxRepeats - 1;
|
|
8120
|
+
}
|
|
8121
|
+
persist() {
|
|
8122
|
+
if (!this.io) return;
|
|
8123
|
+
try {
|
|
8124
|
+
this.io.save({
|
|
8125
|
+
since: Object.fromEntries(this.since),
|
|
8126
|
+
lastFiredAt: Object.fromEntries(this.lastFiredAt),
|
|
8127
|
+
fires: Object.fromEntries(this.fires)
|
|
8128
|
+
});
|
|
8129
|
+
} catch (e) {
|
|
8130
|
+
console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u043D\u0430 \u0434\u0438\u0441\u043A:", e);
|
|
8131
|
+
}
|
|
7922
8132
|
}
|
|
7923
8133
|
};
|
|
8134
|
+
function safeLoad(io) {
|
|
8135
|
+
if (!io) return null;
|
|
8136
|
+
try {
|
|
8137
|
+
return io.load();
|
|
8138
|
+
} catch (e) {
|
|
8139
|
+
console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E (\u043D\u0430\u0447\u0438\u043D\u0430\u044E \u0441 \u0447\u0438\u0441\u0442\u043E\u0433\u043E):", e);
|
|
8140
|
+
return null;
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
7924
8143
|
var REVIEW_LIKE_STATUS = "review";
|
|
7925
8144
|
var MAX_BODY_CHARS = 1800;
|
|
7926
8145
|
function ageMs(now, iso) {
|
|
@@ -7929,6 +8148,12 @@ function ageMs(now, iso) {
|
|
|
7929
8148
|
function runLabel(run) {
|
|
7930
8149
|
return run.name || `#${run.id}`;
|
|
7931
8150
|
}
|
|
8151
|
+
function runTitle(run, what) {
|
|
8152
|
+
return `${run.task_slug || runLabel(run)}: ${what}`;
|
|
8153
|
+
}
|
|
8154
|
+
function droneOf(run) {
|
|
8155
|
+
return `\u0414\u0440\u043E\u043D ${runLabel(run)}`;
|
|
8156
|
+
}
|
|
7932
8157
|
function hrefOf(url) {
|
|
7933
8158
|
return url ? { href: url } : {};
|
|
7934
8159
|
}
|
|
@@ -7946,31 +8171,62 @@ function joinCapped(labels) {
|
|
|
7946
8171
|
}
|
|
7947
8172
|
function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
7948
8173
|
const candidates = [];
|
|
7949
|
-
const allRuns = db.listRuns();
|
|
8174
|
+
const allRuns = db.listRuns().filter((r) => r.superseded_by == null);
|
|
8175
|
+
const killedByOperator = (run) => {
|
|
8176
|
+
const q = opts.stopQuiet;
|
|
8177
|
+
if (!q) return false;
|
|
8178
|
+
const changedAt = sqliteMs(run.updated_at);
|
|
8179
|
+
return changedAt >= q.from && (q.to == null || changedAt <= q.to);
|
|
8180
|
+
};
|
|
7950
8181
|
const recentFailed = allRuns.filter(
|
|
7951
|
-
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs
|
|
8182
|
+
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r)
|
|
7952
8183
|
);
|
|
7953
8184
|
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
7954
8185
|
candidates.push({
|
|
7955
8186
|
key: "failed_spike",
|
|
7956
8187
|
minDurationMs: 0,
|
|
7957
8188
|
build: () => ({
|
|
7958
|
-
|
|
7959
|
-
|
|
8189
|
+
// Единственный ПУШ про падения (TASWM-327): одиночное падение рой перезапустит сам
|
|
8190
|
+
// (оно уходит в ленту, см. ниже), а «падают все» — это уже систематика, где без
|
|
8191
|
+
// человека не поедет. Раньше владелец получал и восемь адресных пушей, и агрегат
|
|
8192
|
+
// поверх них.
|
|
8193
|
+
level: "decision",
|
|
8194
|
+
title: `\u0420\u043E\u0439: ${recentFailed.length} ${plural2(recentFailed.length, ["\u0443\u043F\u0430\u0432\u0448\u0438\u0439 \u043F\u0440\u043E\u0433\u043E\u043D", "\u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430", "\u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432"])} \u0437\u0430 ${Math.round(thresholds.failedSpikeWindowMs / 6e4)} \u043C\u0438\u043D`,
|
|
8195
|
+
body: `${joinCapped(recentFailed.map((r) => r.task_slug || runLabel(r)))} \u2014 \u0440\u043E\u0441\u0442 failed \u0437\u0430 \u043E\u043A\u043D\u043E, \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u0441\u0438\u0441\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0443\u044E \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443, \u0430 \u043D\u0435 \u0435\u0434\u0438\u043D\u0438\u0447\u043D\u044B\u0439 \u0441\u0431\u043E\u0439.`
|
|
7960
8196
|
})
|
|
7961
8197
|
});
|
|
7962
8198
|
}
|
|
8199
|
+
const downBySlug = /* @__PURE__ */ new Map();
|
|
8200
|
+
for (const r of allRuns) {
|
|
8201
|
+
if (r.status !== "failed" && r.status !== "stalled") continue;
|
|
8202
|
+
if (r.resolves_run_id != null || r.reviews_run_id != null) continue;
|
|
8203
|
+
downBySlug.set(r.task_slug, (downBySlug.get(r.task_slug) ?? 0) + 1);
|
|
8204
|
+
}
|
|
8205
|
+
const isRepeatDown = (run) => (downBySlug.get(run.task_slug) ?? 0) >= 2 || run.respawns > 0;
|
|
8206
|
+
const DOWN_GROUP = {
|
|
8207
|
+
id: "run_down",
|
|
8208
|
+
title: (n) => `${n} ${plural2(n, ["\u0434\u0440\u043E\u043D \u043D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B", "\u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B\u0438", "\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B\u0438"])}`
|
|
8209
|
+
};
|
|
8210
|
+
const PR_GROUP = {
|
|
8211
|
+
id: "pr_stuck",
|
|
8212
|
+
title: (n) => `${n} PR ${plural2(n, ["\u0436\u0434\u0451\u0442", "\u0436\u0434\u0443\u0442", "\u0436\u0434\u0443\u0442"])} \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044F`
|
|
8213
|
+
};
|
|
7963
8214
|
for (const run of allRuns) {
|
|
7964
8215
|
if (run.status !== "stalled") continue;
|
|
7965
8216
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8217
|
+
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8218
|
+
if (killedByOperator(run)) continue;
|
|
8219
|
+
const repeat = isRepeatDown(run);
|
|
7966
8220
|
candidates.push({
|
|
7967
8221
|
key: `run_stalled:${run.id}`,
|
|
7968
8222
|
minDurationMs: 0,
|
|
7969
8223
|
// само по себе попадание в stalled УЖЕ прошло свой STALL_MS/RESPAWN_LIMIT
|
|
8224
|
+
group: DOWN_GROUP,
|
|
7970
8225
|
build: () => ({
|
|
7971
8226
|
runId: run.id,
|
|
7972
|
-
|
|
7973
|
-
|
|
8227
|
+
level: repeat ? "decision" : "feed",
|
|
8228
|
+
title: runTitle(run, "\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441"),
|
|
8229
|
+
body: `${droneOf(run)}: \u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u043E\u0438\u0442.${repeat ? " \u041F\u043E \u044D\u0442\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435 \u044D\u0442\u043E \u0443\u0436\u0435 \u0432\u0442\u043E\u0440\u043E\u0439 \u0437\u0430\u0445\u043E\u0434 \u2014 \u0441\u0430\u043C \u043F\u043E \u0441\u0435\u0431\u0435 \u043E\u043D \u043D\u0435 \u0440\u0430\u0437\u043E\u0439\u0434\u0451\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440." : ""}`,
|
|
7974
8230
|
...hrefOf(run.task_url)
|
|
7975
8231
|
})
|
|
7976
8232
|
});
|
|
@@ -7978,13 +8234,18 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
7978
8234
|
for (const run of allRuns) {
|
|
7979
8235
|
if (run.status !== "failed") continue;
|
|
7980
8236
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8237
|
+
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8238
|
+
if (killedByOperator(run)) continue;
|
|
8239
|
+
const repeat = isRepeatDown(run);
|
|
7981
8240
|
candidates.push({
|
|
7982
8241
|
key: `run_failed:${run.id}`,
|
|
7983
8242
|
minDurationMs: 0,
|
|
8243
|
+
group: DOWN_GROUP,
|
|
7984
8244
|
build: () => ({
|
|
7985
8245
|
runId: run.id,
|
|
7986
|
-
|
|
7987
|
-
|
|
8246
|
+
level: repeat ? "decision" : "feed",
|
|
8247
|
+
title: runTitle(run, "\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B"),
|
|
8248
|
+
body: `${droneOf(run)}: \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u043E\u043C\u0435\u0447\u0435\u043D failed.${repeat ? " \u041F\u043E \u044D\u0442\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435 \u043F\u0430\u0434\u0430\u0435\u0442 \u0443\u0436\u0435 \u0432\u0442\u043E\u0440\u043E\u0439 \u0440\u0430\u0437 \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u043D\u0435 \u0432\u044B\u0431\u0435\u0440\u0435\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440." : " \u0420\u043E\u0439 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442 \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0430\u043C."}`,
|
|
7988
8249
|
...hrefOf(run.task_url)
|
|
7989
8250
|
})
|
|
7990
8251
|
});
|
|
@@ -7996,6 +8257,11 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
7996
8257
|
key: "queue_stalled",
|
|
7997
8258
|
minDurationMs: thresholds.queueStalledMs,
|
|
7998
8259
|
build: () => ({
|
|
8260
|
+
// В ленту, без пуша (TASWM-327): «очередь стоит» — состояние, из которого рой
|
|
8261
|
+
// штатно выходит сам (гейт откроется / шторм отпустит / слот освободится), а
|
|
8262
|
+
// настоящий тупик ловит и РАЗБИРАЕТ сторож прогресса (watchdog.ts) — вот он и
|
|
8263
|
+
// звонит человеку, когда исчерпал лестницу.
|
|
8264
|
+
level: "feed",
|
|
7999
8265
|
title: `\u0420\u043E\u0439 \u0441\u0442\u043E\u0438\u0442: \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
|
|
8000
8266
|
body: `${queuedCount} \u043F\u0440\u043E\u0433\u043E\u043D(\u043E\u0432) \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0440\u043E\u043D\u0430 \u2265${Math.round(thresholds.queueStalledMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u043E\u0445\u043E\u0436\u0435, \u0433\u0435\u0439\u0442 (\u0440\u0435\u0441\u0443\u0440\u0441\u044B/maxDrones/\u0448\u0442\u043E\u0440\u043C) \u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043D\u0442\u0435\u0439\u043A \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043E\u043B\u0433\u043E.`
|
|
8001
8267
|
})
|
|
@@ -8007,6 +8273,10 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8007
8273
|
key: "resource-gate-stall",
|
|
8008
8274
|
minDurationMs: thresholds.resourceGateStallMs,
|
|
8009
8275
|
build: () => ({
|
|
8276
|
+
// В ленту: гейт откроется сам, как только машина разгрузится. Отдельный ПУШ про
|
|
8277
|
+
// «машина не тянет, вот цена» шлёт reliefCost.ts — там есть числа и рекомендация,
|
|
8278
|
+
// то есть человеку действительно есть что решать.
|
|
8279
|
+
level: "feed",
|
|
8010
8280
|
title: `\u0420\u043E\u0439 \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442 ${Math.round(thresholds.resourceGateStallMs / 6e4)} \u043C\u0438\u043D \u2014 \u0434\u0435\u0440\u0436\u0438\u0442 \u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442`,
|
|
8011
8281
|
body: `${queuedCount} \u043F\u0440\u043E\u0433\u043E\u043D(\u043E\u0432) \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0436\u0434\u0443\u0442 \u0441\u043F\u0430\u0432\u043D\u0430: ${reason}. \u0420\u0430\u0437\u0433\u0440\u0443\u0437\u0438 \u043C\u0430\u0448\u0438\u043D\u0443.`
|
|
8012
8282
|
})
|
|
@@ -8020,10 +8290,13 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8020
8290
|
candidates.push({
|
|
8021
8291
|
key: `pr_stuck_merge:${run.id}`,
|
|
8022
8292
|
minDurationMs: thresholds.prStuckMergeMs,
|
|
8293
|
+
group: PR_GROUP,
|
|
8023
8294
|
build: () => ({
|
|
8024
8295
|
runId: run.id,
|
|
8025
|
-
|
|
8026
|
-
|
|
8296
|
+
// Пуш: ревью одобрено, дальше ход человека — ровно тот сигнал, который тонул в шуме.
|
|
8297
|
+
level: "decision",
|
|
8298
|
+
title: runTitle(run, "PR \u043E\u0434\u043E\u0431\u0440\u0435\u043D, \u043D\u043E \u043D\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D"),
|
|
8299
|
+
body: `${droneOf(run)}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B APPROVE \u2265${Math.round(thresholds.prStuckMergeMs / 6e4)} \u043C\u0438\u043D \u043D\u0430\u0437\u0430\u0434, PR \u0432\u0441\u0451 \u0435\u0449\u0451 \u043E\u0442\u043A\u0440\u044B\u0442 \u2014 ${run.pr_url}.`,
|
|
8027
8300
|
...hrefOf(run.task_url)
|
|
8028
8301
|
})
|
|
8029
8302
|
});
|
|
@@ -8031,10 +8304,14 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8031
8304
|
candidates.push({
|
|
8032
8305
|
key: `pr_no_reviewer:${run.id}`,
|
|
8033
8306
|
minDurationMs: thresholds.prNoReviewerMs,
|
|
8307
|
+
group: PR_GROUP,
|
|
8034
8308
|
build: () => ({
|
|
8035
8309
|
runId: run.id,
|
|
8036
|
-
|
|
8037
|
-
|
|
8310
|
+
// В ленту: ревьюер не поднялся из-за гейта (cap/maxDrones/cutoff) — как только
|
|
8311
|
+
// гейт откроется, рой поднимет его сам, решения человека тут не требуется.
|
|
8312
|
+
level: "feed",
|
|
8313
|
+
title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
|
|
8314
|
+
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 (SWARM_REVIEW_CAP/maxDrones/cutoff?) \u2014 ${run.pr_url}.`,
|
|
8038
8315
|
...hrefOf(run.task_url)
|
|
8039
8316
|
})
|
|
8040
8317
|
});
|
|
@@ -8042,10 +8319,14 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8042
8319
|
candidates.push({
|
|
8043
8320
|
key: `pr_stuck_changes:${run.id}`,
|
|
8044
8321
|
minDurationMs: thresholds.prNoReviewerMs,
|
|
8322
|
+
group: PR_GROUP,
|
|
8045
8323
|
build: () => ({
|
|
8046
8324
|
runId: run.id,
|
|
8047
|
-
|
|
8048
|
-
|
|
8325
|
+
// Пуш: конвейер ревью встал так, что рой этого сам не расшивает (дрон умер на
|
|
8326
|
+
// фиксе до done()) — дальше без человека никак.
|
|
8327
|
+
level: "decision",
|
|
8328
|
+
title: runTitle(run, "PR \u0437\u0430\u0441\u0442\u0440\u044F\u043B \u043D\u0430 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB"),
|
|
8329
|
+
body: `${droneOf(run)}: \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044F \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u043D\u043E \u043D\u0438 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430, \u043D\u0438 \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u2014 \u0440\u0435\u0432\u044C\u044E \u0432\u0441\u0442\u0430\u043B\u043E (\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 \u043D\u0430 \u0444\u0438\u043A\u0441\u0435 \u0434\u043E done()?) \u2014 ${run.pr_url}.`,
|
|
8049
8330
|
...hrefOf(run.task_url)
|
|
8050
8331
|
})
|
|
8051
8332
|
});
|
|
@@ -8054,14 +8335,21 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8054
8335
|
const runsById = new Map(allRuns.map((r) => [r.id, r]));
|
|
8055
8336
|
for (const q of db.openQuestions()) {
|
|
8056
8337
|
const run = runsById.get(q.run_id);
|
|
8338
|
+
if (!run) continue;
|
|
8057
8339
|
candidates.push({
|
|
8058
8340
|
key: `question_unanswered:${q.id}`,
|
|
8059
8341
|
minDurationMs: thresholds.questionUnansweredMs,
|
|
8342
|
+
group: {
|
|
8343
|
+
id: "question_unanswered",
|
|
8344
|
+
title: (n) => `${n} ${plural2(n, ["\u0434\u0440\u043E\u043D \u0436\u0434\u0451\u0442", "\u0434\u0440\u043E\u043D\u0430 \u0436\u0434\u0443\u0442", "\u0434\u0440\u043E\u043D\u043E\u0432 \u0436\u0434\u0443\u0442"])} \u043E\u0442\u0432\u0435\u0442\u0430`
|
|
8345
|
+
},
|
|
8060
8346
|
build: () => ({
|
|
8061
8347
|
runId: q.run_id,
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8348
|
+
// Пуш — это ровно «без тебя не поедет»: дрон стоит и жжёт время сессии.
|
|
8349
|
+
level: "decision",
|
|
8350
|
+
title: runTitle(run, "\u0434\u0440\u043E\u043D \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430"),
|
|
8351
|
+
body: `${droneOf(run)} \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2265${Math.round(thresholds.questionUnansweredMs / 6e4)} \u043C\u0438\u043D: \xAB${q.text}\xBB.`,
|
|
8352
|
+
...hrefOf(run.task_url)
|
|
8065
8353
|
})
|
|
8066
8354
|
});
|
|
8067
8355
|
}
|
|
@@ -8071,6 +8359,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8071
8359
|
key: "outbox_stuck",
|
|
8072
8360
|
minDurationMs: thresholds.outboxStuckMs,
|
|
8073
8361
|
build: () => ({
|
|
8362
|
+
// В ленту: недоступный прод рой переживает сам (outbox durable, флашится, когда
|
|
8363
|
+
// связь вернётся) — человеку тут нечего решать.
|
|
8364
|
+
level: "feed",
|
|
8074
8365
|
title: `Outbox \u0440\u043E\u044F \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
|
|
8075
8366
|
body: `${pending2} \u043E\u043F\u0435\u0440\u0430\u0446\u0438(\u0439) \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E \u0434\u043E \u043F\u0440\u043E\u0434\u0430 \u2265${Math.round(thresholds.outboxStuckMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u0440\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0434\u043E\u043B\u044C\u0448\u0435 \u043E\u0431\u044B\u0447\u043D\u043E\u0433\u043E \u0441\u0431\u043E\u044F.`
|
|
8076
8367
|
})
|
|
@@ -8146,7 +8437,14 @@ async function liftNotes(opts, result) {
|
|
|
8146
8437
|
continue;
|
|
8147
8438
|
}
|
|
8148
8439
|
try {
|
|
8149
|
-
await opts.notify({
|
|
8440
|
+
await opts.notify({
|
|
8441
|
+
key,
|
|
8442
|
+
level: "note",
|
|
8443
|
+
title: `${run.task_slug || `#${run.id}`}: \u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438`,
|
|
8444
|
+
body: `\u0414\u0440\u043E\u043D ${run.name || `#${run.id}`}: ${text}`,
|
|
8445
|
+
...hrefOf(opts.hrefFor?.(run.id)),
|
|
8446
|
+
runId: run.id
|
|
8447
|
+
});
|
|
8150
8448
|
opts.seenNotes.add(key);
|
|
8151
8449
|
result.notes.push(run.id);
|
|
8152
8450
|
} catch (e) {
|
|
@@ -8357,8 +8655,8 @@ function makeExclusiveReader(opts) {
|
|
|
8357
8655
|
}
|
|
8358
8656
|
|
|
8359
8657
|
// src/envGate.ts
|
|
8360
|
-
import { existsSync as
|
|
8361
|
-
import { join as
|
|
8658
|
+
import { existsSync as existsSync10 } from "fs";
|
|
8659
|
+
import { join as join16, delimiter } from "path";
|
|
8362
8660
|
import { createConnection } from "net";
|
|
8363
8661
|
import { execa as execa2 } from "execa";
|
|
8364
8662
|
var PROBE_TIMEOUT_MS = 2500;
|
|
@@ -8409,8 +8707,8 @@ var COMPOSE_MARKERS = [
|
|
|
8409
8707
|
"compose.yaml"
|
|
8410
8708
|
];
|
|
8411
8709
|
function detectRequirements(repo, io = {}) {
|
|
8412
|
-
const exists = io.exists ??
|
|
8413
|
-
const marker = COMPOSE_MARKERS.find((f) => exists(
|
|
8710
|
+
const exists = io.exists ?? existsSync10;
|
|
8711
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join16(repo, f)));
|
|
8414
8712
|
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
8415
8713
|
}
|
|
8416
8714
|
function resolveRequirements(repo, declared, io = {}) {
|
|
@@ -8429,10 +8727,10 @@ async function dockerAlive() {
|
|
|
8429
8727
|
}
|
|
8430
8728
|
}
|
|
8431
8729
|
async function hasCommand(name, env) {
|
|
8432
|
-
if (name.includes("/")) return
|
|
8730
|
+
if (name.includes("/")) return existsSync10(name);
|
|
8433
8731
|
const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
8434
8732
|
const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
8435
|
-
return dirs.some((dir) => exts.some((ext) =>
|
|
8733
|
+
return dirs.some((dir) => exts.some((ext) => existsSync10(join16(dir, name + ext))));
|
|
8436
8734
|
}
|
|
8437
8735
|
async function portOpen(host, port) {
|
|
8438
8736
|
return new Promise((resolve3) => {
|
|
@@ -8550,7 +8848,7 @@ import { serve as serve2 } from "@hono/node-server";
|
|
|
8550
8848
|
|
|
8551
8849
|
// src/diff.ts
|
|
8552
8850
|
import { statSync as statSync5 } from "fs";
|
|
8553
|
-
import { join as
|
|
8851
|
+
import { join as join17 } from "path";
|
|
8554
8852
|
import { $ as $4 } from "execa";
|
|
8555
8853
|
var MAX_FILES = 200;
|
|
8556
8854
|
var MAX_LINES_PER_FILE = 1500;
|
|
@@ -8718,7 +9016,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
8718
9016
|
files.push(pf);
|
|
8719
9017
|
}
|
|
8720
9018
|
}
|
|
8721
|
-
files.sort((a, b) => mtime(
|
|
9019
|
+
files.sort((a, b) => mtime(join17(worktree, b.path)) - mtime(join17(worktree, a.path)));
|
|
8722
9020
|
const truncatedFiles = files.length > MAX_FILES;
|
|
8723
9021
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
8724
9022
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -8734,12 +9032,71 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
8734
9032
|
};
|
|
8735
9033
|
}
|
|
8736
9034
|
|
|
9035
|
+
// package.json
|
|
9036
|
+
var package_default = {
|
|
9037
|
+
name: "@taskless-app/swarm",
|
|
9038
|
+
version: "0.4.2",
|
|
9039
|
+
type: "module",
|
|
9040
|
+
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
9041
|
+
keywords: [
|
|
9042
|
+
"ai-agents",
|
|
9043
|
+
"coding-agents",
|
|
9044
|
+
"swarm",
|
|
9045
|
+
"orchestration",
|
|
9046
|
+
"autonomous-agents",
|
|
9047
|
+
"mcp",
|
|
9048
|
+
"taskless",
|
|
9049
|
+
"cli"
|
|
9050
|
+
],
|
|
9051
|
+
license: "SEE LICENSE IN LICENSE",
|
|
9052
|
+
author: "Ilya Smurov <ilya@smurov.com>",
|
|
9053
|
+
homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
|
|
9054
|
+
repository: {
|
|
9055
|
+
type: "git",
|
|
9056
|
+
url: "git+https://github.com/ilyasmurov/taskless.git",
|
|
9057
|
+
directory: "packages/swarm-daemon"
|
|
9058
|
+
},
|
|
9059
|
+
engines: {
|
|
9060
|
+
node: ">=22"
|
|
9061
|
+
},
|
|
9062
|
+
bin: {
|
|
9063
|
+
swarm: "dist/cli.js"
|
|
9064
|
+
},
|
|
9065
|
+
files: [
|
|
9066
|
+
"dist",
|
|
9067
|
+
"docs/HERO.md",
|
|
9068
|
+
"roles"
|
|
9069
|
+
],
|
|
9070
|
+
scripts: {
|
|
9071
|
+
overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
|
|
9072
|
+
swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
|
|
9073
|
+
build: "tsup",
|
|
9074
|
+
start: "node dist/overmind.js",
|
|
9075
|
+
prepublishOnly: "pnpm run build",
|
|
9076
|
+
test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
|
|
9077
|
+
"test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
|
|
9078
|
+
"test:watch": "vitest",
|
|
9079
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
9080
|
+
lint: "echo 'no lint in swarm-daemon yet'"
|
|
9081
|
+
},
|
|
9082
|
+
dependencies: {
|
|
9083
|
+
"@hono/node-server": "^2.0.12",
|
|
9084
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
9085
|
+
execa: "^10.0.0",
|
|
9086
|
+
zod: "3.25.76"
|
|
9087
|
+
},
|
|
9088
|
+
optionalDependencies: {
|
|
9089
|
+
"better-sqlite3": "^12.11.1"
|
|
9090
|
+
}
|
|
9091
|
+
};
|
|
9092
|
+
|
|
8737
9093
|
// src/web.ts
|
|
8738
9094
|
init_storm();
|
|
8739
9095
|
init_resources();
|
|
8740
9096
|
init_config();
|
|
8741
9097
|
init_droneChannel();
|
|
8742
9098
|
init_oauth();
|
|
9099
|
+
var PACKAGE_VERSION = package_default.version;
|
|
8743
9100
|
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
8744
9101
|
var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
8745
9102
|
var TASKLESS_MARK = `<svg viewBox="0 0 60 54" width="19" height="19" fill="none" aria-hidden="true" style="display:block"><g transform="matrix(1.0512,0,0,1.0512,0,-0.021)"><path fill="currentColor" fill-rule="nonzero" d="M46.92,0 L10.24,0 C4.59,0 0,4.59 0,10.24 L0,10.4 C0,16.05 4.59,20.64 10.24,20.64 L16.84,20.64 C19.56,20.64 22.12,19.59 24.05,17.67 L36.06,5.77 C37.25,4.6 38.82,3.95 40.49,3.95 L46.93,3.95 C50.4,3.95 53.22,6.77 53.22,10.24 L53.22,10.4 C53.22,13.87 50.4,16.69 46.93,16.69 L39.82,16.69 C37.05,16.69 34.36,17.83 32.44,19.83 L21.13,31.58 C19.28,33.5 18.27,36.02 18.27,38.68 L18.27,40.94 C18.27,46.59 22.86,51.18 28.51,51.18 C34.16,51.18 38.75,46.59 38.75,40.94 L38.75,20.74 C39.1,20.68 39.46,20.64 39.82,20.64 L46.93,20.64 C52.58,20.64 57.17,16.05 57.17,10.4 L57.17,10.24 C57.17,4.59 52.58,0 46.93,0 L46.92,0 Z M21.26,14.87 C20.07,16.04 18.5,16.69 16.83,16.69 L10.23,16.69 C6.76,16.69 3.94,13.87 3.94,10.4 L3.94,10.24 C3.94,6.77 6.76,3.95 10.23,3.95 L32.27,3.95 L21.25,14.87 L21.26,14.87 Z M34.79,40.94 C34.79,44.41 31.97,47.23 28.5,47.23 C25.03,47.23 22.21,44.41 22.21,40.94 L22.21,38.68 C22.21,37.05 22.83,35.5 23.97,34.32 L34.79,23.07 L34.79,40.94 L34.79,40.94 Z"/></g></svg>`;
|
|
@@ -10020,6 +10377,36 @@ async function runDiff(db, id, now = Date.now()) {
|
|
|
10020
10377
|
}
|
|
10021
10378
|
var readNarratorLog = (log) => log?.() ?? [];
|
|
10022
10379
|
var readRestartSnap = (restart, liveSessionIds2) => restart ? { phase: restart.phase, sessionIds: liveSessionIds2?.() ?? [] } : null;
|
|
10380
|
+
function pauseSourceOf(raw) {
|
|
10381
|
+
return raw === "cli" || raw === "remote" ? raw : "web";
|
|
10382
|
+
}
|
|
10383
|
+
function setPausedVia(deps, paused, source, reason) {
|
|
10384
|
+
const control = deps.control;
|
|
10385
|
+
if (control.set) {
|
|
10386
|
+
control.set(paused, { source, reason });
|
|
10387
|
+
return;
|
|
10388
|
+
}
|
|
10389
|
+
control.paused = paused;
|
|
10390
|
+
deps.narrate?.(
|
|
10391
|
+
paused ? "\u23F8 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0443 \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u043C, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442." : "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0441\u043D\u044F\u043B \u0440\u043E\u0439 \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0451\u043C \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438 \u0438\u043D\u0442\u0435\u0439\u043A\u0430."
|
|
10392
|
+
);
|
|
10393
|
+
}
|
|
10394
|
+
function daemonStatus(deps) {
|
|
10395
|
+
const counts = {};
|
|
10396
|
+
for (const r of deps.db.listRuns()) counts[r.status] = (counts[r.status] ?? 0) + 1;
|
|
10397
|
+
const live = deps.liveSessionIds?.() ?? [];
|
|
10398
|
+
return {
|
|
10399
|
+
hostId: deps.selfHost?.hostId ?? null,
|
|
10400
|
+
hostName: deps.selfHost?.hostName ?? null,
|
|
10401
|
+
backend: SWARM_BACKEND,
|
|
10402
|
+
paused: deps.control?.paused ?? false,
|
|
10403
|
+
pause: deps.control?.state ?? null,
|
|
10404
|
+
restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
|
|
10405
|
+
counts,
|
|
10406
|
+
live,
|
|
10407
|
+
version: PACKAGE_VERSION
|
|
10408
|
+
};
|
|
10409
|
+
}
|
|
10023
10410
|
function eventsStream(db, narratorLog, control, daemonVersion, restart, liveSessionIds2, selfHost) {
|
|
10024
10411
|
let iv;
|
|
10025
10412
|
let last = "";
|
|
@@ -10188,18 +10575,25 @@ function makeHandler(deps) {
|
|
|
10188
10575
|
return redirect("/");
|
|
10189
10576
|
}
|
|
10190
10577
|
if (p === "/pause" && req.method === "POST") {
|
|
10191
|
-
if (!
|
|
10578
|
+
if (!authed(req)) return unauthorized();
|
|
10192
10579
|
if (!deps.control) return new Response("\u043F\u0430\u0443\u0437\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u043C\u043E\u0440\u0434\u0430 \u0431\u0435\u0437 \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430", { status: 503 });
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10580
|
+
const f = await formValue(req);
|
|
10581
|
+
const reason = (f.get("reason") ?? "").trim() || null;
|
|
10582
|
+
setPausedVia(deps, true, pauseSourceOf(f.get("source")), reason);
|
|
10583
|
+
return Response.json({ ok: true, paused: true, why: "\u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u043C, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442" });
|
|
10196
10584
|
}
|
|
10197
10585
|
if (p === "/resume" && req.method === "POST") {
|
|
10198
|
-
if (!
|
|
10586
|
+
if (!authed(req)) return unauthorized();
|
|
10199
10587
|
if (!deps.control) return new Response("\u043F\u0430\u0443\u0437\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u043C\u043E\u0440\u0434\u0430 \u0431\u0435\u0437 \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430", { status: 503 });
|
|
10200
|
-
deps.
|
|
10201
|
-
|
|
10202
|
-
|
|
10588
|
+
setPausedVia(deps, false, pauseSourceOf((await formValue(req)).get("source")), null);
|
|
10589
|
+
return Response.json({ ok: true, paused: false, why: "\u0440\u043E\u0439 \u0441\u043D\u044F\u0442 \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0451\u043C \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438 \u0438\u043D\u0442\u0435\u0439\u043A\u0430" });
|
|
10590
|
+
}
|
|
10591
|
+
if (p === "/status") return Response.json(daemonStatus(deps));
|
|
10592
|
+
if (p === "/shutdown" && req.method === "POST") {
|
|
10593
|
+
if (!authed(req)) return unauthorized();
|
|
10594
|
+
if (!deps.requestStop) return new Response("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u043C\u043E\u0440\u0434\u0430 \u0431\u0435\u0437 \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430", { status: 503 });
|
|
10595
|
+
const f = await formValue(req);
|
|
10596
|
+
return Response.json(deps.requestStop({ force: f.get("force") === "1", wait: f.get("wait") === "1" }));
|
|
10203
10597
|
}
|
|
10204
10598
|
if (p === "/revive" && req.method === "POST") {
|
|
10205
10599
|
if (!auth()) return unauthorized();
|
|
@@ -10372,6 +10766,115 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
10372
10766
|
}
|
|
10373
10767
|
}
|
|
10374
10768
|
|
|
10769
|
+
// src/anomalyState.ts
|
|
10770
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14, renameSync as renameSync6, writeFileSync as writeFileSync11 } from "fs";
|
|
10771
|
+
import { join as join18 } from "path";
|
|
10772
|
+
var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
10773
|
+
var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
|
|
10774
|
+
function anomalyStatePath(home) {
|
|
10775
|
+
return join18(home, "anomaly-state.json");
|
|
10776
|
+
}
|
|
10777
|
+
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
10778
|
+
const exists = io.exists ?? existsSync11;
|
|
10779
|
+
const readFile2 = io.readFile ?? ((p) => readFileSync14(p, "utf8"));
|
|
10780
|
+
const path = anomalyStatePath(home);
|
|
10781
|
+
if (!exists(path)) return { ...EMPTY };
|
|
10782
|
+
try {
|
|
10783
|
+
const raw = JSON.parse(readFile2(path));
|
|
10784
|
+
const since = numbers(raw.since);
|
|
10785
|
+
const lastFiredAt = numbers(raw.lastFiredAt);
|
|
10786
|
+
const fires = numbers(raw.fires);
|
|
10787
|
+
const fresh = new Set(
|
|
10788
|
+
[...Object.keys(since), ...Object.keys(lastFiredAt)].filter(
|
|
10789
|
+
(k) => now - Math.max(since[k] ?? 0, lastFiredAt[k] ?? 0) <= KEY_TTL_MS
|
|
10790
|
+
)
|
|
10791
|
+
);
|
|
10792
|
+
return {
|
|
10793
|
+
since: pick(since, fresh),
|
|
10794
|
+
lastFiredAt: pick(lastFiredAt, fresh),
|
|
10795
|
+
fires: pick(fires, fresh),
|
|
10796
|
+
stopQuiet: parseQuiet(raw.stopQuiet)
|
|
10797
|
+
};
|
|
10798
|
+
} catch {
|
|
10799
|
+
return { ...EMPTY };
|
|
10800
|
+
}
|
|
10801
|
+
}
|
|
10802
|
+
function saveAnomalyState(home, state, io = {}) {
|
|
10803
|
+
const path = anomalyStatePath(home);
|
|
10804
|
+
const writeFile2 = io.writeFile ?? ((p, data) => {
|
|
10805
|
+
writeFileSync11(`${p}.tmp`, data);
|
|
10806
|
+
renameSync6(`${p}.tmp`, p);
|
|
10807
|
+
});
|
|
10808
|
+
writeFile2(path, JSON.stringify(state, null, 2));
|
|
10809
|
+
}
|
|
10810
|
+
var AnomalyStateStore = class {
|
|
10811
|
+
constructor(home, now = Date.now(), io = {}) {
|
|
10812
|
+
this.home = home;
|
|
10813
|
+
this.io = io;
|
|
10814
|
+
this.state = loadAnomalyState(home, now, io);
|
|
10815
|
+
}
|
|
10816
|
+
home;
|
|
10817
|
+
io;
|
|
10818
|
+
state;
|
|
10819
|
+
/** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
|
|
10820
|
+
trackerIo() {
|
|
10821
|
+
return {
|
|
10822
|
+
load: () => ({ since: this.state.since, lastFiredAt: this.state.lastFiredAt, fires: this.state.fires }),
|
|
10823
|
+
save: (snapshot) => {
|
|
10824
|
+
this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
|
|
10825
|
+
this.flush();
|
|
10826
|
+
}
|
|
10827
|
+
};
|
|
10828
|
+
}
|
|
10829
|
+
/** Окно штатной остановки для buildAnomalyCandidates (null — рой ни разу не останавливали). */
|
|
10830
|
+
stopQuiet() {
|
|
10831
|
+
return this.state.stopQuiet;
|
|
10832
|
+
}
|
|
10833
|
+
/**
|
|
10834
|
+
* Рой ушёл на паузу/остановку — открываем окно тишины. Повторный вызов на уже открытом
|
|
10835
|
+
* окне ничего не меняет: пауза, поставленная дважды, это одна пауза.
|
|
10836
|
+
*/
|
|
10837
|
+
markPaused(now) {
|
|
10838
|
+
const open = this.state.stopQuiet;
|
|
10839
|
+
if (open && open.to == null) return;
|
|
10840
|
+
this.state = { ...this.state, stopQuiet: { from: now, to: null } };
|
|
10841
|
+
this.flush();
|
|
10842
|
+
}
|
|
10843
|
+
/** Рой снят с паузы — закрываем окно. Окна не было (рестарт потерял) → ничего не выдумываем. */
|
|
10844
|
+
markResumed(now) {
|
|
10845
|
+
const open = this.state.stopQuiet;
|
|
10846
|
+
if (!open || open.to != null) return;
|
|
10847
|
+
this.state = { ...this.state, stopQuiet: { from: open.from, to: now } };
|
|
10848
|
+
this.flush();
|
|
10849
|
+
}
|
|
10850
|
+
flush() {
|
|
10851
|
+
try {
|
|
10852
|
+
saveAnomalyState(this.home, this.state, this.io);
|
|
10853
|
+
} catch (e) {
|
|
10854
|
+
console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u043D\u0430 \u0434\u0438\u0441\u043A:", e);
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
};
|
|
10858
|
+
function numbers(raw) {
|
|
10859
|
+
if (!raw || typeof raw !== "object") return {};
|
|
10860
|
+
const out = {};
|
|
10861
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
10862
|
+
if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
10863
|
+
}
|
|
10864
|
+
return out;
|
|
10865
|
+
}
|
|
10866
|
+
function pick(src, keys) {
|
|
10867
|
+
const out = {};
|
|
10868
|
+
for (const k of keys) if (src[k] != null) out[k] = src[k];
|
|
10869
|
+
return out;
|
|
10870
|
+
}
|
|
10871
|
+
function parseQuiet(raw) {
|
|
10872
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10873
|
+
const q = raw;
|
|
10874
|
+
if (typeof q.from !== "number" || !Number.isFinite(q.from)) return null;
|
|
10875
|
+
return { from: q.from, to: typeof q.to === "number" && Number.isFinite(q.to) ? q.to : null };
|
|
10876
|
+
}
|
|
10877
|
+
|
|
10375
10878
|
// src/queueOrder.ts
|
|
10376
10879
|
function compareQueuePriority(a, b) {
|
|
10377
10880
|
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
@@ -10737,7 +11240,7 @@ async function escalateStaleCiWaits(deps, now) {
|
|
|
10737
11240
|
if (deps.watchdog?.notify) {
|
|
10738
11241
|
const signal = {
|
|
10739
11242
|
key: `ci_wait_stale:${run.id}:${since}`,
|
|
10740
|
-
title:
|
|
11243
|
+
title: signalTitle(run, `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0436\u0434\u0451\u0442 CI \u0434\u043E\u043B\u044C\u0448\u0435 ${minutes} \u043C\u0438\u043D`),
|
|
10741
11244
|
body: `${run.task_slug}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0448\u0442\u0430\u0442\u043D\u043E \u0432\u044B\u0448\u0435\u043B \u0436\u0434\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 PR, \u043D\u043E \u043E\u043D\u0438 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C \u0437\u0430 ${minutes} \u043C\u0438\u043D (\u0437\u0430\u0441\u0442\u0440\u044F\u043B\u0438 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u0430\u043D\u043D\u0435\u0440\u043E\u0432 / \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u044B / gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442). \u0412\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u0440\u043E\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442. ${until} \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C: ${run.pr_url ?? "PR \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D"}.`,
|
|
10742
11245
|
runId: run.id,
|
|
10743
11246
|
...hrefOf(run.task_url)
|
|
@@ -10942,10 +11445,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
10942
11445
|
}
|
|
10943
11446
|
return parts.join("\n");
|
|
10944
11447
|
}
|
|
10945
|
-
var HERO_CHARTER_PATH =
|
|
11448
|
+
var HERO_CHARTER_PATH = join19(dirname5(THIS_FILE), "..", "docs", "HERO.md");
|
|
10946
11449
|
function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
|
|
10947
11450
|
const name = run.name || `#${run.id}`;
|
|
10948
|
-
const charter =
|
|
11451
|
+
const charter = readFileSync15(HERO_CHARTER_PATH, "utf8");
|
|
10949
11452
|
const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
|
|
10950
11453
|
|
|
10951
11454
|
${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
|
|
@@ -11085,6 +11588,9 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
|
|
|
11085
11588
|
function droneLabel(run) {
|
|
11086
11589
|
return run.name || `#${run.id}`;
|
|
11087
11590
|
}
|
|
11591
|
+
function signalTitle(run, what) {
|
|
11592
|
+
return `${run.task_slug || droneLabel(run)}: ${what}`;
|
|
11593
|
+
}
|
|
11088
11594
|
function narrate(deps, text) {
|
|
11089
11595
|
try {
|
|
11090
11596
|
deps.narrate?.(text);
|
|
@@ -11193,7 +11699,7 @@ async function wakeRun(deps, runId) {
|
|
|
11193
11699
|
if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
|
|
11194
11700
|
if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
|
|
11195
11701
|
if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
|
|
11196
|
-
const worktreeExists = deps.worktreeExists ??
|
|
11702
|
+
const worktreeExists = deps.worktreeExists ?? existsSync12;
|
|
11197
11703
|
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
11198
11704
|
const wt = run.worktree_path;
|
|
11199
11705
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -11278,7 +11784,7 @@ function repoHasCi(deps, run) {
|
|
|
11278
11784
|
function repoHasCiWorkflows(worktreePath, repo) {
|
|
11279
11785
|
for (const base3 of [worktreePath, repo]) {
|
|
11280
11786
|
if (!base3) continue;
|
|
11281
|
-
const dir =
|
|
11787
|
+
const dir = join19(base3, ".github", "workflows");
|
|
11282
11788
|
try {
|
|
11283
11789
|
if (readdirSync9(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
|
|
11284
11790
|
} catch {
|
|
@@ -11394,8 +11900,12 @@ async function wakeReviewerOnCiReady(deps, run, over) {
|
|
|
11394
11900
|
if (deps.watchdog?.notify) {
|
|
11395
11901
|
const signal = {
|
|
11396
11902
|
key: `reviewer_ci_wake_failed:${reviewer.id}`,
|
|
11397
|
-
title:
|
|
11398
|
-
|
|
11903
|
+
title: signalTitle(run, "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u0442\u0435\u0440\u044F\u043B \u0441\u0435\u0441\u0441\u0438\u044E \u043D\u0430 CI-\u043F\u0430\u0443\u0437\u0435"),
|
|
11904
|
+
// В ленту, без пуша (TASWM-327): следующая же строка тела и есть ответ — рой сам
|
|
11905
|
+
// поднимает свежего ревьюера на том же PR. Решать человеку тут нечего; если и
|
|
11906
|
+
// свежий не доедет, конвейер встанет и об этом скажет уже pr_stuck_* пушем.
|
|
11907
|
+
level: "feed",
|
|
11908
|
+
body: `\u0420\u0435\u0432\u044C\u044E\u0435\u0440 ${droneLabel(reviewer)} \u0434\u043E\u0436\u0434\u0430\u043B\u0441\u044F \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}). \u041F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR \u2014 ${run.pr_url ?? "?"}.`,
|
|
11399
11909
|
runId: reviewer.id,
|
|
11400
11910
|
...hrefOf(run.task_url)
|
|
11401
11911
|
};
|
|
@@ -11423,7 +11933,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
|
|
|
11423
11933
|
return true;
|
|
11424
11934
|
}
|
|
11425
11935
|
function worktreeAndTranscriptAlive(deps, worktree) {
|
|
11426
|
-
const exists = deps.worktreeExists ??
|
|
11936
|
+
const exists = deps.worktreeExists ?? existsSync12;
|
|
11427
11937
|
const transcript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
11428
11938
|
return exists(worktree) && transcript(worktree);
|
|
11429
11939
|
}
|
|
@@ -11628,7 +12138,7 @@ async function resumeWatchdogParked(deps, droneCap) {
|
|
|
11628
12138
|
}
|
|
11629
12139
|
if (!candidate) return;
|
|
11630
12140
|
if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
|
|
11631
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12141
|
+
const worktreeExists = deps.worktreeExists ?? existsSync12;
|
|
11632
12142
|
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
11633
12143
|
const wt = candidate.worktree_path;
|
|
11634
12144
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -11797,7 +12307,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
11797
12307
|
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
11798
12308
|
if (!deps.respawn) return;
|
|
11799
12309
|
const wt = resumeCandidate.worktree_path;
|
|
11800
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12310
|
+
const worktreeExists = deps.worktreeExists ?? existsSync12;
|
|
11801
12311
|
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
11802
12312
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
11803
12313
|
const mode = canContinue ? "continue" : "fresh";
|
|
@@ -11949,7 +12459,7 @@ function buildEscalationCandidates(db) {
|
|
|
11949
12459
|
// маркер сам по себе УЖЕ прошёл свой RESOLVE_LIMIT/REVIEW_SPAWN_LIMIT/…
|
|
11950
12460
|
build: () => ({
|
|
11951
12461
|
runId: run.id,
|
|
11952
|
-
title:
|
|
12462
|
+
title: signalTitle(run, "\u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440"),
|
|
11953
12463
|
body: `${run.task_slug}: ${hit.text ?? "\u044D\u0441\u043A\u0430\u043B\u0430\u0446\u0438\u044F \u0431\u0435\u0437 \u0442\u0435\u043A\u0441\u0442\u0430"} \u2014 ${run.pr_url ?? "?"}.`,
|
|
11954
12464
|
...hrefOf(run.task_url)
|
|
11955
12465
|
})
|
|
@@ -11959,7 +12469,7 @@ function buildEscalationCandidates(db) {
|
|
|
11959
12469
|
}
|
|
11960
12470
|
async function tickOnce(deps) {
|
|
11961
12471
|
const now = deps.now ? deps.now() : Date.now();
|
|
11962
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12472
|
+
const worktreeExists = deps.worktreeExists ?? existsSync12;
|
|
11963
12473
|
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
11964
12474
|
if (deps.rehydrate) {
|
|
11965
12475
|
const rh = deps.rehydrate;
|
|
@@ -12091,7 +12601,7 @@ async function tickOnce(deps) {
|
|
|
12091
12601
|
if (!already) {
|
|
12092
12602
|
const signal = {
|
|
12093
12603
|
key: `daemon_self_reference:${run.id}`,
|
|
12094
|
-
title:
|
|
12604
|
+
title: signalTitle(run, "PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430"),
|
|
12095
12605
|
body: `${run.task_slug}: \u043F\u0440\u0430\u0432\u043A\u0430 \u0437\u0430\u0434\u0435\u0432\u0430\u0435\u0442 ${DAEMON_CODE_PATHS.join("/, ")}/ \u2014 \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 (TASWM-251) \u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0430\u0442\u0442\u0435\u0441\u0442\u043E\u0432\u0430\u0442\u044C \u0435\u0451 \u043E\u0431\u044B\u0447\u043D\u044B\u043C \u043F\u0443\u0442\u0451\u043C. \u0410\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u2014 ${run.pr_url}.`,
|
|
12096
12606
|
runId: run.id,
|
|
12097
12607
|
...hrefOf(run.task_url)
|
|
@@ -12449,9 +12959,9 @@ async function tickOnce(deps) {
|
|
|
12449
12959
|
if (deps.respawn && deps.resourcesOk && !stormActive) {
|
|
12450
12960
|
await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
|
|
12451
12961
|
}
|
|
12452
|
-
if (deps.anomalies) {
|
|
12962
|
+
if (deps.anomalies && !deps.control?.paused) {
|
|
12453
12963
|
const az = deps.anomalies;
|
|
12454
|
-
let
|
|
12964
|
+
let ready = [];
|
|
12455
12965
|
try {
|
|
12456
12966
|
const candidates = [
|
|
12457
12967
|
...buildAnomalyCandidates(deps.db, now, az.thresholds, {
|
|
@@ -12459,29 +12969,31 @@ async function tickOnce(deps) {
|
|
|
12459
12969
|
...az.outboxPendingCount ? { outboxPendingCount: az.outboxPendingCount() } : {},
|
|
12460
12970
|
// resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
|
|
12461
12971
|
// deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
|
|
12462
|
-
...deps.gateReason ? { gateReason: deps.gateReason() } : {}
|
|
12972
|
+
...deps.gateReason ? { gateReason: deps.gateReason() } : {},
|
|
12973
|
+
...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
|
|
12463
12974
|
}),
|
|
12464
12975
|
...buildEscalationCandidates(deps.db),
|
|
12465
12976
|
// ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
|
|
12466
12977
|
// труба, тот же дедуп «один раз на эпизод». Пустой список при нормальной работе.
|
|
12467
12978
|
...await buildReliefCostCandidatesFor(deps, now, droneCap)
|
|
12468
12979
|
];
|
|
12469
|
-
|
|
12980
|
+
ready = collapseSignals(az.tracker.eligible(now, candidates));
|
|
12470
12981
|
} catch (e) {
|
|
12471
12982
|
console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0434\u0435\u0442\u0435\u043A\u0442 \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
12472
12983
|
}
|
|
12473
|
-
for (const
|
|
12474
|
-
const
|
|
12984
|
+
for (const item of ready) {
|
|
12985
|
+
const finalCall = item.keys.some((k) => az.tracker.isFinalRepeat(k));
|
|
12986
|
+
const signal = finalCall ? { ...item.signal, body: `${item.signal.body} ${LAST_REMINDER_SUFFIX}` } : item.signal;
|
|
12475
12987
|
try {
|
|
12476
12988
|
await az.notify(signal);
|
|
12477
12989
|
} catch (e) {
|
|
12478
12990
|
console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
|
|
12479
12991
|
continue;
|
|
12480
12992
|
}
|
|
12481
|
-
az.tracker.commit(
|
|
12482
|
-
|
|
12993
|
+
for (const key of item.keys) az.tracker.commit(key, now);
|
|
12994
|
+
for (const ev of item.events) {
|
|
12483
12995
|
try {
|
|
12484
|
-
deps.db.addEvent(
|
|
12996
|
+
deps.db.addEvent(ev.runId, "anomaly", ev.text);
|
|
12485
12997
|
} catch (e) {
|
|
12486
12998
|
console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0432\u0435\u0445\u0430 \xAB${signal.key}\xBB \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u0430\u0441\u044C:`, e);
|
|
12487
12999
|
}
|
|
@@ -12563,7 +13075,7 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
12563
13075
|
workspace_id: reviewRun.workspace_id
|
|
12564
13076
|
// та же задача, тот же воркспейс (TASWM-288)
|
|
12565
13077
|
});
|
|
12566
|
-
const wt =
|
|
13078
|
+
const wt = join19(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
|
|
12567
13079
|
try {
|
|
12568
13080
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
12569
13081
|
await deps.git.setWorktreeIdentity(wt, resolver.name);
|
|
@@ -12615,7 +13127,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
12615
13127
|
// (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
|
|
12616
13128
|
workspace_id: reviewRun.workspace_id
|
|
12617
13129
|
});
|
|
12618
|
-
const wt =
|
|
13130
|
+
const wt = join19(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
12619
13131
|
try {
|
|
12620
13132
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
12621
13133
|
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
@@ -12650,7 +13162,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
12650
13162
|
]);
|
|
12651
13163
|
const base3 = effectiveBaseBranch(run.base_branch, projectBase);
|
|
12652
13164
|
const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
|
|
12653
|
-
if (!
|
|
13165
|
+
if (!existsSync12(wt)) {
|
|
12654
13166
|
const branch = runToSpawn.branch || runToSpawn.task_slug;
|
|
12655
13167
|
const source = await addWorktree(repo, wt, branch, {
|
|
12656
13168
|
...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
|
|
@@ -12679,7 +13191,7 @@ ${notice}` : freshPrompt;
|
|
|
12679
13191
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
12680
13192
|
}
|
|
12681
13193
|
async function syncCrashedOnStartup(deps) {
|
|
12682
|
-
const worktreeExists = deps.worktreeExists ??
|
|
13194
|
+
const worktreeExists = deps.worktreeExists ?? existsSync12;
|
|
12683
13195
|
let orphaned = 0;
|
|
12684
13196
|
let merged = 0;
|
|
12685
13197
|
let requeued = 0;
|
|
@@ -12759,7 +13271,7 @@ async function migrateWorktreesAtStartup() {
|
|
|
12759
13271
|
legacyDir: WORKTREES_LEGACY_DIR,
|
|
12760
13272
|
targetDir: WORKTREES_DIR,
|
|
12761
13273
|
envOverride: process.env.SWARM_WORKTREES || "",
|
|
12762
|
-
exists: (d) =>
|
|
13274
|
+
exists: (d) => existsSync12(d),
|
|
12763
13275
|
isNonEmptyDir: (d) => {
|
|
12764
13276
|
try {
|
|
12765
13277
|
return readdirSync9(d).length > 0;
|
|
@@ -12768,13 +13280,13 @@ async function migrateWorktreesAtStartup() {
|
|
|
12768
13280
|
}
|
|
12769
13281
|
},
|
|
12770
13282
|
liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
|
|
12771
|
-
rename: (from, to) =>
|
|
13283
|
+
rename: (from, to) => renameSync7(from, to),
|
|
12772
13284
|
repairMoved: (t) => repairMovedWorktrees(t),
|
|
12773
13285
|
log: (m) => console.log(m)
|
|
12774
13286
|
});
|
|
12775
13287
|
} catch (e) {
|
|
12776
13288
|
console.error("[migrate] \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F worktrees \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u2014 \u0444\u043E\u043B\u0431\u044D\u043A:", e);
|
|
12777
|
-
return
|
|
13289
|
+
return existsSync12(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
|
|
12778
13290
|
}
|
|
12779
13291
|
}
|
|
12780
13292
|
async function reportWorkspaceAccess(tl, db) {
|
|
@@ -12868,7 +13380,7 @@ async function runDaemon(opts) {
|
|
|
12868
13380
|
const client = db;
|
|
12869
13381
|
heartbeat = {
|
|
12870
13382
|
push: async () => {
|
|
12871
|
-
const commands = await client.heartbeat(daemonVersion?.get() ?? null);
|
|
13383
|
+
const commands = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
|
|
12872
13384
|
if (commands.length === 0) return;
|
|
12873
13385
|
await serveLogRequests(
|
|
12874
13386
|
{
|
|
@@ -12912,11 +13424,15 @@ async function runDaemon(opts) {
|
|
|
12912
13424
|
console.warn("[overmind] \u043C\u043E\u0441\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0434\u0451\u043D (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F sqlite \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430) \u2014 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432 \u043E\u0441\u0442\u0430\u043D\u0443\u0442\u0441\u044F \u0432 \u043D\u0435\u0439:", e);
|
|
12913
13425
|
}
|
|
12914
13426
|
}
|
|
13427
|
+
const anomalyState = new AnomalyStateStore(SWARM_HOME);
|
|
12915
13428
|
let anomalies;
|
|
12916
13429
|
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
12917
13430
|
const client = db;
|
|
12918
13431
|
anomalies = {
|
|
12919
|
-
tracker: new AnomalyTracker(SWARM_ANOMALY_RENOTIFY_MS
|
|
13432
|
+
tracker: new AnomalyTracker(SWARM_ANOMALY_RENOTIFY_MS, {
|
|
13433
|
+
maxRepeats: SWARM_ANOMALY_MAX_REPEATS,
|
|
13434
|
+
io: anomalyState.trackerIo()
|
|
13435
|
+
}),
|
|
12920
13436
|
thresholds: {
|
|
12921
13437
|
failedSpikeCount: SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
12922
13438
|
failedSpikeWindowMs: SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
@@ -12925,11 +13441,13 @@ async function runDaemon(opts) {
|
|
|
12925
13441
|
prNoReviewerMs: SWARM_ANOMALY_PR_NO_REVIEWER_MS,
|
|
12926
13442
|
questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
|
|
12927
13443
|
outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS,
|
|
12928
|
-
resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS
|
|
13444
|
+
resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
|
|
13445
|
+
runDownWindowMs: SWARM_ANOMALY_RUN_DOWN_WINDOW_MS
|
|
12929
13446
|
},
|
|
12930
13447
|
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
12931
13448
|
outboxPendingCount: () => client.pendingCount(),
|
|
12932
|
-
notify: (signal) => client.notifyAnomaly(signal)
|
|
13449
|
+
notify: (signal) => client.notifyAnomaly(signal),
|
|
13450
|
+
stopQuiet: () => anomalyState.stopQuiet()
|
|
12933
13451
|
};
|
|
12934
13452
|
}
|
|
12935
13453
|
const watchdog = {
|
|
@@ -13031,7 +13549,33 @@ async function runDaemon(opts) {
|
|
|
13031
13549
|
reviewCutoffAtValue = reviewCutoffAt();
|
|
13032
13550
|
}
|
|
13033
13551
|
const narrator = makeOvermindNarrator();
|
|
13034
|
-
const
|
|
13552
|
+
const bootPause = loadPauseState(SWARM_HOME);
|
|
13553
|
+
const control = { paused: bootPause.paused, state: bootPause.paused ? bootPause : null };
|
|
13554
|
+
if (bootPause.paused) {
|
|
13555
|
+
console.log(`[overmind] \u23F8 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u041D\u0410 \u041F\u0410\u0423\u0417\u0415: ${pauseText(bootPause)}. \u0421\u043D\u044F\u0442\u044C: swarm resume`);
|
|
13556
|
+
const pausedAt = Date.parse(bootPause.at);
|
|
13557
|
+
anomalyState.markPaused(Number.isNaN(pausedAt) ? Date.now() : pausedAt);
|
|
13558
|
+
} else {
|
|
13559
|
+
anomalyState.markResumed(Date.now());
|
|
13560
|
+
}
|
|
13561
|
+
const setPaused = (paused, opts2) => {
|
|
13562
|
+
const next = paused ? makePauseState(opts2.source, opts2.reason ?? null) : null;
|
|
13563
|
+
control.paused = paused;
|
|
13564
|
+
control.state = next;
|
|
13565
|
+
if (paused) anomalyState.markPaused(Date.now());
|
|
13566
|
+
else anomalyState.markResumed(Date.now());
|
|
13567
|
+
try {
|
|
13568
|
+
savePauseState(SWARM_HOME, next ?? { paused: false, at: "", source: opts2.source });
|
|
13569
|
+
} catch (e) {
|
|
13570
|
+
console.error(`[overmind] \u043F\u0430\u0443\u0437\u0430 \u041D\u0415 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u0430 \u043D\u0430 \u0434\u0438\u0441\u043A (${e instanceof Error ? e.message : e}) \u2014 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0451\u0442 \u0442\u0438\u043A\u0438, \u043D\u043E \u043D\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430`);
|
|
13571
|
+
}
|
|
13572
|
+
narrator.narrate(
|
|
13573
|
+
paused ? `\u23F8 \u0420\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 (${opts2.source === "cli" ? "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0441 \u043C\u0430\u0448\u0438\u043D\u044B" : opts2.source === "remote" ? "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless" : "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u043C\u043E\u0440\u0434\u0430"})${opts2.reason ? `: ${opts2.reason}` : ""} \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u043C, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442.` : "\u25B6\uFE0F \u0420\u043E\u0439 \u0441\u043D\u044F\u0442 \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0451\u043C \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438 \u0438\u043D\u0442\u0435\u0439\u043A\u0430."
|
|
13574
|
+
);
|
|
13575
|
+
void heartbeat?.push().catch(() => {
|
|
13576
|
+
});
|
|
13577
|
+
};
|
|
13578
|
+
control.set = setPaused;
|
|
13035
13579
|
const restart = initialRestartState();
|
|
13036
13580
|
const deps = {
|
|
13037
13581
|
db,
|
|
@@ -13217,7 +13761,7 @@ async function runDaemon(opts) {
|
|
|
13217
13761
|
mode,
|
|
13218
13762
|
nudge
|
|
13219
13763
|
),
|
|
13220
|
-
worktreeExists: (p) =>
|
|
13764
|
+
worktreeExists: (p) => existsSync12(p),
|
|
13221
13765
|
hasTranscript: (p) => sessionTranscriptExists(p),
|
|
13222
13766
|
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
13223
13767
|
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|
|
@@ -13257,31 +13801,39 @@ async function runDaemon(opts) {
|
|
|
13257
13801
|
};
|
|
13258
13802
|
let web;
|
|
13259
13803
|
let droneChannel = null;
|
|
13260
|
-
function performRestart(orphanedIds) {
|
|
13804
|
+
function performRestart(orphanedIds, intent = "restart") {
|
|
13261
13805
|
if (restart.phase === "restarting") return;
|
|
13262
13806
|
restart.phase = "restarting";
|
|
13807
|
+
restart.intent = intent;
|
|
13808
|
+
const stopping = intent === "stop";
|
|
13263
13809
|
if (orphanedIds.length) {
|
|
13264
13810
|
for (const id of orphanedIds) {
|
|
13265
13811
|
db.addEvent(
|
|
13266
13812
|
id,
|
|
13267
13813
|
"anomaly",
|
|
13268
|
-
"\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C, \u043F\u043E\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u044F \u0431\u044B\u043B\u0430 \u0436\u0438\u0432\u0430 \u2014 heartbeat-\u043A\u0430\u043D\u0430\u043B \u043F\u0440\u0435\u0440\u0432\u0430\u043B\u0441\u044F \u043D\u0430 \u0432\u0440\u0435\u043C\u044F \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 (TASWM-250)"
|
|
13814
|
+
stopping ? "\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C (swarm stop --force), \u043F\u043E\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u044F \u0431\u044B\u043B\u0430 \u0436\u0438\u0432\u0430 \u2014 \u043E\u043D\u0430 \u0437\u0430\u043C\u0440\u0451\u0442 \u043D\u0430 \u0431\u043B\u0438\u0436\u0430\u0439\u0448\u0435\u043C \u043E\u0431\u0440\u0430\u0449\u0435\u043D\u0438\u0438 \u043A \u0434\u0435\u043C\u043E\u043D\u0443 (TASWM-326)" : "\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C, \u043F\u043E\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u044F \u0431\u044B\u043B\u0430 \u0436\u0438\u0432\u0430 \u2014 heartbeat-\u043A\u0430\u043D\u0430\u043B \u043F\u0440\u0435\u0440\u0432\u0430\u043B\u0441\u044F \u043D\u0430 \u0432\u0440\u0435\u043C\u044F \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 (TASWM-250)"
|
|
13269
13815
|
);
|
|
13270
13816
|
}
|
|
13271
|
-
narrator.narrate(
|
|
13817
|
+
narrator.narrate(
|
|
13818
|
+
stopping ? `\u23F9 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0433\u0430\u0441\u0438\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u2014 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u043E \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${orphanedIds.length}` : `\u{1F501} \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0444\u043E\u0440\u0441\u0438\u0440\u0443\u0435\u0442 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u043E \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${orphanedIds.length}`
|
|
13819
|
+
);
|
|
13272
13820
|
} else {
|
|
13273
|
-
narrator.narrate("\u{1F501} \u0414\u0435\u043C\u043E\u043D \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u2014 \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442");
|
|
13821
|
+
narrator.narrate(stopping ? "\u23F9 \u0414\u0435\u043C\u043E\u043D \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C \u2014 \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442" : "\u{1F501} \u0414\u0435\u043C\u043E\u043D \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u2014 \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442");
|
|
13274
13822
|
}
|
|
13275
13823
|
const isCompiled = THIS_FILE.endsWith(".js");
|
|
13276
13824
|
const respawnArgs = isCompiled ? [THIS_FILE] : ["--conditions=development", resolveTsxCli(), THIS_FILE];
|
|
13277
13825
|
setTimeout(() => {
|
|
13278
13826
|
Promise.all([closeWebServer(web.server), droneChannel ? closeDroneChannel(droneChannel.server) : Promise.resolve()]).then(() => {
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13827
|
+
if (!stopping) {
|
|
13828
|
+
spawn3(process.execPath, respawnArgs, {
|
|
13829
|
+
detached: true,
|
|
13830
|
+
stdio: "ignore",
|
|
13831
|
+
cwd: process.cwd(),
|
|
13832
|
+
env: { ...process.env, SWARM_REPO: opts.repo }
|
|
13833
|
+
}).unref();
|
|
13834
|
+
} else {
|
|
13835
|
+
console.log("[overmind] \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 `swarm stop`. \u041F\u043E\u0434\u043D\u044F\u0442\u044C \u043E\u0431\u0440\u0430\u0442\u043D\u043E: swarm start");
|
|
13836
|
+
}
|
|
13285
13837
|
process.exit(0);
|
|
13286
13838
|
});
|
|
13287
13839
|
}, 300);
|
|
@@ -13295,22 +13847,43 @@ async function runDaemon(opts) {
|
|
|
13295
13847
|
}
|
|
13296
13848
|
restart.phase = "waiting";
|
|
13297
13849
|
restart.requestedAt = Date.now();
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
restartAutoPaused = true;
|
|
13301
|
-
}
|
|
13850
|
+
restart.intent = "restart";
|
|
13851
|
+
autoPauseForWait();
|
|
13302
13852
|
narrator.narrate(`\u23F3 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u0438\u043B \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${sessions.length}`);
|
|
13303
13853
|
return { waiting: true, sessions };
|
|
13304
13854
|
};
|
|
13855
|
+
const requestStop = (o) => {
|
|
13856
|
+
const sessions = liveSessionIds(db, pidAlive);
|
|
13857
|
+
if (o.force || sessions.length === 0) {
|
|
13858
|
+
performRestart(o.force ? sessions : [], "stop");
|
|
13859
|
+
return { stopping: true, waiting: false, sessions };
|
|
13860
|
+
}
|
|
13861
|
+
if (!o.wait) return { stopping: false, waiting: false, sessions };
|
|
13862
|
+
restart.phase = "waiting";
|
|
13863
|
+
restart.requestedAt = Date.now();
|
|
13864
|
+
restart.intent = "stop";
|
|
13865
|
+
autoPauseForWait();
|
|
13866
|
+
narrator.narrate(`\u23F3 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0433\u0430\u0441\u0438\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${sessions.length}`);
|
|
13867
|
+
return { stopping: false, waiting: true, sessions };
|
|
13868
|
+
};
|
|
13869
|
+
function autoPauseForWait() {
|
|
13870
|
+
if (control.paused) return;
|
|
13871
|
+
control.paused = true;
|
|
13872
|
+
restartAutoPaused = true;
|
|
13873
|
+
anomalyState.markPaused(Date.now());
|
|
13874
|
+
}
|
|
13305
13875
|
const cancelRestartWait = () => {
|
|
13306
13876
|
if (restart.phase !== "waiting") return;
|
|
13877
|
+
const wasStop = restart.intent === "stop";
|
|
13307
13878
|
restart.phase = "idle";
|
|
13308
13879
|
restart.requestedAt = null;
|
|
13880
|
+
restart.intent = "restart";
|
|
13309
13881
|
if (restartAutoPaused) {
|
|
13310
13882
|
control.paused = false;
|
|
13311
13883
|
restartAutoPaused = false;
|
|
13884
|
+
anomalyState.markResumed(Date.now());
|
|
13312
13885
|
}
|
|
13313
|
-
narrator.narrate("\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430");
|
|
13886
|
+
narrator.narrate(wasStop ? "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u0434\u0435\u043C\u043E\u043D\u0430" : "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430");
|
|
13314
13887
|
};
|
|
13315
13888
|
if (channelSecret) {
|
|
13316
13889
|
droneChannel = startDroneChannel(
|
|
@@ -13319,10 +13892,13 @@ async function runDaemon(opts) {
|
|
|
13319
13892
|
secret: channelSecret,
|
|
13320
13893
|
log: (m) => console.log(m),
|
|
13321
13894
|
...db instanceof SwarmClient ? {
|
|
13895
|
+
// level: "note" — та же дисциплина, что у моста (TASWM-327): заметка идёт в
|
|
13896
|
+
// ленту, никогда пушем; заголовок — со слага задачи, имя дрона в теле.
|
|
13322
13897
|
onNote: (run, text) => db.notifyAnomaly({
|
|
13323
13898
|
key: noteNotifyKey(run.id, text),
|
|
13324
|
-
|
|
13325
|
-
|
|
13899
|
+
level: "note",
|
|
13900
|
+
title: `${run.task_slug || `#${run.id}`}: \u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438`,
|
|
13901
|
+
body: `\u0414\u0440\u043E\u043D ${run.name || `#${run.id}`}: ${text}`,
|
|
13326
13902
|
...hrefOf(run.task_url),
|
|
13327
13903
|
runId: run.id
|
|
13328
13904
|
})
|
|
@@ -13366,6 +13942,9 @@ async function runDaemon(opts) {
|
|
|
13366
13942
|
liveSessionIds: () => liveSessionIds(db, pidAlive),
|
|
13367
13943
|
requestRestart,
|
|
13368
13944
|
cancelRestartWait,
|
|
13945
|
+
// `swarm stop` (TASWM-326) — та же механика ожидания, что у рестарта, но в конце
|
|
13946
|
+
// процесс выходит. Штатная замена `pkill -f overmind`.
|
|
13947
|
+
requestStop,
|
|
13369
13948
|
// Побудка прогона по id (TASWM-240): вход для `swarm wake <id>` — тот же respawn с
|
|
13370
13949
|
// полными deps демона (getTask/verifyMode/реестр репо), что и авто-оживление, и по
|
|
13371
13950
|
// зеркалу демона, а не по короткоживущему стору CLI.
|
|
@@ -13413,7 +13992,7 @@ async function runDaemon(opts) {
|
|
|
13413
13992
|
writeHeartbeat(HEARTBEAT_PATH, Date.now());
|
|
13414
13993
|
await tickOnce(deps);
|
|
13415
13994
|
if (restart.phase === "waiting" && liveSessionIds(db, pidAlive).length === 0) {
|
|
13416
|
-
performRestart([]);
|
|
13995
|
+
performRestart([], restart.intent);
|
|
13417
13996
|
}
|
|
13418
13997
|
} catch (e) {
|
|
13419
13998
|
console.error("[overmind] tick error:", e);
|