@taskless-app/swarm 0.6.14 → 0.6.15
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 +3 -1
- package/dist/cli.js +362 -172
- package/dist/overmind.js +793 -327
- package/package.json +2 -2
package/dist/overmind.js
CHANGED
|
@@ -317,21 +317,28 @@ function computeReviewPhase(input) {
|
|
|
317
317
|
return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
|
|
318
318
|
}
|
|
319
319
|
const p = input.ciProgress;
|
|
320
|
+
if (p && p.total > 0 && p.pass >= p.total) {
|
|
321
|
+
return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
|
|
322
|
+
}
|
|
320
323
|
const label = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
|
|
321
324
|
return { kind: "checks", label, ready: false };
|
|
322
325
|
}
|
|
323
|
-
var REVIEW_PHASE_TEXT;
|
|
326
|
+
var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
|
|
324
327
|
var init_reviewPhase = __esm({
|
|
325
328
|
"../swarm-core/src/reviewPhase.ts"() {
|
|
326
329
|
"use strict";
|
|
327
330
|
REVIEW_PHASE_TEXT = {
|
|
328
331
|
checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
|
|
332
|
+
awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
|
|
329
333
|
ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
|
|
330
334
|
in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
|
|
331
335
|
changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
|
|
332
336
|
ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
|
|
333
337
|
reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
|
|
334
338
|
};
|
|
339
|
+
PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
|
|
340
|
+
(a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
|
|
341
|
+
);
|
|
335
342
|
}
|
|
336
343
|
});
|
|
337
344
|
|
|
@@ -511,7 +518,7 @@ var init_status = __esm({
|
|
|
511
518
|
// ../swarm-core/src/deps.ts
|
|
512
519
|
function judgeDep(runs) {
|
|
513
520
|
const real = runs.filter((r) => !r.service);
|
|
514
|
-
if (real.length === 0) return { ...
|
|
521
|
+
if (real.length === 0) return { ...UNKNOWN };
|
|
515
522
|
if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
|
|
516
523
|
if (real.some((r) => r.status === "done")) {
|
|
517
524
|
return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
|
|
@@ -531,12 +538,13 @@ function depClearedText(slug, v) {
|
|
|
531
538
|
const why = v.stopReason ? ` \u2014 ${v.stopReason}` : "";
|
|
532
539
|
return `\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043D\u044F\u0442\u0430: \u0436\u0434\u0430\u043B ${who}, \u0442\u043E\u0442 ${how}${why}. \u0423\u0441\u043F\u0435\u0448\u043D\u043E\u0433\u043E \u0444\u0438\u043D\u0430\u043B\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u2014 \u0435\u0434\u0443.`;
|
|
533
540
|
}
|
|
534
|
-
var PENDING, DEP_END_LABEL;
|
|
541
|
+
var PENDING, UNKNOWN, DEP_END_LABEL;
|
|
535
542
|
var init_deps = __esm({
|
|
536
543
|
"../swarm-core/src/deps.ts"() {
|
|
537
544
|
"use strict";
|
|
538
545
|
init_status();
|
|
539
546
|
PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
|
|
547
|
+
UNKNOWN = { outcome: "unknown", met: false, status: null, name: null, stopReason: null };
|
|
540
548
|
DEP_END_LABEL = {
|
|
541
549
|
stopped: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
|
|
542
550
|
failed: "\u0443\u043F\u0430\u043B",
|
|
@@ -555,6 +563,9 @@ function stampFromEpochSec(sec) {
|
|
|
555
563
|
function sqliteMs(ts) {
|
|
556
564
|
return Date.parse(ts.replace(" ", "T") + "Z");
|
|
557
565
|
}
|
|
566
|
+
function stampMs(at) {
|
|
567
|
+
return at.includes("T") ? Date.parse(at) : sqliteMs(at);
|
|
568
|
+
}
|
|
558
569
|
function toStamp(v) {
|
|
559
570
|
if (typeof v !== "string" || !v) return null;
|
|
560
571
|
const d = new Date(v);
|
|
@@ -1263,31 +1274,6 @@ function buildZoneModel(entries, opts = {}) {
|
|
|
1263
1274
|
const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
|
|
1264
1275
|
return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
|
|
1265
1276
|
}
|
|
1266
|
-
function zoneForFiles(model, files) {
|
|
1267
|
-
if (!hasZoneModel(model) || !files.length) return null;
|
|
1268
|
-
const spineSet = new Set(model.spine);
|
|
1269
|
-
const touchedSpine = files.filter((f) => spineSet.has(f));
|
|
1270
|
-
const votes = /* @__PURE__ */ new Map();
|
|
1271
|
-
for (const f of new Set(files)) {
|
|
1272
|
-
const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
|
|
1273
|
-
if (z2) bump(votes, z2);
|
|
1274
|
-
}
|
|
1275
|
-
const top = topKey(votes);
|
|
1276
|
-
if (!top) {
|
|
1277
|
-
return {
|
|
1278
|
-
zone: SPINE_ZONE,
|
|
1279
|
-
confidence: 1,
|
|
1280
|
-
why: `\u0432\u0441\u0435 \u0444\u0430\u0439\u043B\u044B \u2014 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${touchedSpine.slice(0, 3).join(", ")})`,
|
|
1281
|
-
spine: true
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
return {
|
|
1285
|
-
zone: top.key,
|
|
1286
|
-
confidence: top.share,
|
|
1287
|
-
why: `${top.count} \u0438\u0437 ${[...votes.values()].reduce((a, b) => a + b, 0)} \u0444\u0430\u0439\u043B\u043E\u0432 \u2014 \u0437\u043E\u043D\u0430 ${top.key}`,
|
|
1288
|
-
spine: touchedSpine.length > 0
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
1277
|
function suggestZonesFromText(model, text, limit = 3) {
|
|
1292
1278
|
if (!hasZoneModel(model)) return [];
|
|
1293
1279
|
const terms = tokenize(text);
|
|
@@ -2105,6 +2091,7 @@ __export(config_exports, {
|
|
|
2105
2091
|
REPO_MAP_PATH: () => REPO_MAP_PATH,
|
|
2106
2092
|
REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
|
|
2107
2093
|
RUN_LOGS_DIR: () => RUN_LOGS_DIR,
|
|
2094
|
+
SWARM_ANOMALY_DEP_MISSING_MS: () => SWARM_ANOMALY_DEP_MISSING_MS,
|
|
2108
2095
|
SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
2109
2096
|
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
2110
2097
|
SWARM_ANOMALY_MAX_REPEATS: () => SWARM_ANOMALY_MAX_REPEATS,
|
|
@@ -2147,6 +2134,7 @@ __export(config_exports, {
|
|
|
2147
2134
|
SWARM_REPO: () => SWARM_REPO,
|
|
2148
2135
|
SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
|
|
2149
2136
|
SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
|
|
2137
|
+
SWARM_REPO_SCAN_TTL_MS: () => SWARM_REPO_SCAN_TTL_MS,
|
|
2150
2138
|
SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
|
|
2151
2139
|
SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
|
|
2152
2140
|
SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
|
|
@@ -2168,6 +2156,7 @@ __export(config_exports, {
|
|
|
2168
2156
|
WORKTREES_LEGACY_DIR: () => WORKTREES_LEGACY_DIR,
|
|
2169
2157
|
WORKTREES_NOINDEX_DIR: () => WORKTREES_NOINDEX_DIR,
|
|
2170
2158
|
droneWorktreePath: () => droneWorktreePath,
|
|
2159
|
+
droneWorktreeRoot: () => droneWorktreeRoot,
|
|
2171
2160
|
mcpEndpoint: () => mcpEndpoint,
|
|
2172
2161
|
migrateWorktreesToNoindex: () => migrateWorktreesToNoindex,
|
|
2173
2162
|
normalizeRepo: () => normalizeRepo,
|
|
@@ -2186,7 +2175,10 @@ function normalizeRepo(repo) {
|
|
|
2186
2175
|
}
|
|
2187
2176
|
}
|
|
2188
2177
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
2189
|
-
return join5(worktreesDir,
|
|
2178
|
+
return join5(droneWorktreeRoot(worktreesDir, repo), slug);
|
|
2179
|
+
}
|
|
2180
|
+
function droneWorktreeRoot(worktreesDir, repo) {
|
|
2181
|
+
return join5(worktreesDir, basename(normalizeRepo(repo)));
|
|
2190
2182
|
}
|
|
2191
2183
|
function resolveEffectiveWorktreesDir(deps) {
|
|
2192
2184
|
if (deps.envOverride) return deps.envOverride;
|
|
@@ -2248,7 +2240,7 @@ function reviewCutoffAt() {
|
|
|
2248
2240
|
}
|
|
2249
2241
|
return at;
|
|
2250
2242
|
}
|
|
2251
|
-
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, 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;
|
|
2243
|
+
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
|
|
2252
2244
|
var init_config = __esm({
|
|
2253
2245
|
"src/config.ts"() {
|
|
2254
2246
|
"use strict";
|
|
@@ -2310,6 +2302,7 @@ var init_config = __esm({
|
|
|
2310
2302
|
SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
|
|
2311
2303
|
REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
|
|
2312
2304
|
SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
|
|
2305
|
+
SWARM_REPO_SCAN_TTL_MS = envNum("SWARM_REPO_SCAN_TTL_MS", 24 * 60 * 6e4);
|
|
2313
2306
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
2314
2307
|
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
2315
2308
|
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
@@ -2336,6 +2329,7 @@ var init_config = __esm({
|
|
|
2336
2329
|
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
2337
2330
|
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
2338
2331
|
SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
|
|
2332
|
+
SWARM_ANOMALY_DEP_MISSING_MS = envNum("SWARM_ANOMALY_DEP_MISSING_MS", 2 * 6e4);
|
|
2339
2333
|
SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
|
|
2340
2334
|
}
|
|
2341
2335
|
});
|
|
@@ -2734,11 +2728,11 @@ var init_storm = __esm({
|
|
|
2734
2728
|
});
|
|
2735
2729
|
|
|
2736
2730
|
// src/heartbeat.ts
|
|
2737
|
-
import { readFileSync as readFileSync5, renameSync as
|
|
2731
|
+
import { readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
2738
2732
|
function writeHeartbeat(path, now) {
|
|
2739
2733
|
const tmp = `${path}.tmp`;
|
|
2740
2734
|
writeFileSync5(tmp, String(now));
|
|
2741
|
-
|
|
2735
|
+
renameSync4(tmp, path);
|
|
2742
2736
|
}
|
|
2743
2737
|
function readHeartbeat(path) {
|
|
2744
2738
|
try {
|
|
@@ -2761,7 +2755,7 @@ var init_heartbeat = __esm({
|
|
|
2761
2755
|
|
|
2762
2756
|
// src/agentToken.ts
|
|
2763
2757
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
2764
|
-
import { join as
|
|
2758
|
+
import { join as join9 } from "path";
|
|
2765
2759
|
function loadAgentToken() {
|
|
2766
2760
|
try {
|
|
2767
2761
|
const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
|
|
@@ -2817,7 +2811,7 @@ var init_agentToken = __esm({
|
|
|
2817
2811
|
"src/agentToken.ts"() {
|
|
2818
2812
|
"use strict";
|
|
2819
2813
|
init_config();
|
|
2820
|
-
STORE_PATH =
|
|
2814
|
+
STORE_PATH = join9(SWARM_HOME, "agent.json");
|
|
2821
2815
|
base = (api) => api.replace(/\/$/, "");
|
|
2822
2816
|
TOKEN_PROBE_TIMEOUT_MS = 8e3;
|
|
2823
2817
|
}
|
|
@@ -3177,9 +3171,9 @@ var init_procCleanup = __esm({
|
|
|
3177
3171
|
});
|
|
3178
3172
|
|
|
3179
3173
|
// src/git.ts
|
|
3180
|
-
import { existsSync as
|
|
3174
|
+
import { existsSync as existsSync10, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
|
|
3181
3175
|
import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3182
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3176
|
+
import { isAbsolute as isAbsolute2, join as join17, sep as sep2 } from "path";
|
|
3183
3177
|
import { $ as $2 } from "execa";
|
|
3184
3178
|
function isUnrecoverableRefError(e) {
|
|
3185
3179
|
if (e instanceof MissingRefError) return true;
|
|
@@ -3191,7 +3185,7 @@ async function reclaimStaleWorktree(repo, worktree) {
|
|
|
3191
3185
|
}
|
|
3192
3186
|
function isLinkedWorktree(dir) {
|
|
3193
3187
|
try {
|
|
3194
|
-
return
|
|
3188
|
+
return statSync8(join17(dir, ".git")).isFile();
|
|
3195
3189
|
} catch {
|
|
3196
3190
|
return false;
|
|
3197
3191
|
}
|
|
@@ -3203,7 +3197,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
3203
3197
|
}
|
|
3204
3198
|
}
|
|
3205
3199
|
async function ensureWorktreeDirGone(worktree) {
|
|
3206
|
-
if (!
|
|
3200
|
+
if (!existsSync10(worktree)) return false;
|
|
3207
3201
|
if (!isLinkedWorktree(worktree)) {
|
|
3208
3202
|
console.error(
|
|
3209
3203
|
`[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.`
|
|
@@ -3218,7 +3212,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
3218
3212
|
);
|
|
3219
3213
|
return true;
|
|
3220
3214
|
}
|
|
3221
|
-
if (
|
|
3215
|
+
if (existsSync10(worktree)) {
|
|
3222
3216
|
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.`);
|
|
3223
3217
|
}
|
|
3224
3218
|
return true;
|
|
@@ -3266,7 +3260,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
3266
3260
|
}
|
|
3267
3261
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
3268
3262
|
if (busyAt) {
|
|
3269
|
-
const exists =
|
|
3263
|
+
const exists = existsSync10(busyAt);
|
|
3270
3264
|
const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
|
|
3271
3265
|
if (live) throw new WorktreeBusyError(branch, busyAt);
|
|
3272
3266
|
if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
|
|
@@ -3301,8 +3295,8 @@ async function ensureGitExcluded(worktree, pattern) {
|
|
|
3301
3295
|
try {
|
|
3302
3296
|
const commonDir = await gitPath(worktree, "--git-common-dir");
|
|
3303
3297
|
if (!commonDir) return false;
|
|
3304
|
-
const infoDir =
|
|
3305
|
-
const excludeFile =
|
|
3298
|
+
const infoDir = join17(commonDir, "info");
|
|
3299
|
+
const excludeFile = join17(infoDir, "exclude");
|
|
3306
3300
|
let current = "";
|
|
3307
3301
|
try {
|
|
3308
3302
|
current = await readFile(excludeFile, "utf8");
|
|
@@ -3322,7 +3316,7 @@ async function gitPath(worktree, flag) {
|
|
|
3322
3316
|
const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
|
|
3323
3317
|
const raw = out.stdout.trim();
|
|
3324
3318
|
if (out.exitCode !== 0 || !raw) return null;
|
|
3325
|
-
return isAbsolute2(raw) ? raw :
|
|
3319
|
+
return isAbsolute2(raw) ? raw : join17(worktree, raw);
|
|
3326
3320
|
}
|
|
3327
3321
|
async function ensureWorktreeHygiene(worktree) {
|
|
3328
3322
|
for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
|
|
@@ -3332,12 +3326,12 @@ async function installGuardHooks(worktree) {
|
|
|
3332
3326
|
try {
|
|
3333
3327
|
const gitDir = await gitPath(worktree, "--git-dir");
|
|
3334
3328
|
if (!gitDir) return false;
|
|
3335
|
-
const hooksDir =
|
|
3329
|
+
const hooksDir = join17(gitDir, "swarm-hooks");
|
|
3336
3330
|
const prev = await previousHooksPath(worktree);
|
|
3337
3331
|
await mkdir(hooksDir, { recursive: true });
|
|
3338
3332
|
for (const hook of GUARD_HOOKS) {
|
|
3339
|
-
await writeFile(
|
|
3340
|
-
await chmod(
|
|
3333
|
+
await writeFile(join17(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
|
|
3334
|
+
await chmod(join17(hooksDir, hook), 493);
|
|
3341
3335
|
}
|
|
3342
3336
|
await $2`git -C ${worktree} config extensions.worktreeConfig true`;
|
|
3343
3337
|
await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
|
|
@@ -3356,7 +3350,7 @@ async function previousHooksPath(worktree) {
|
|
|
3356
3350
|
if (out.exitCode === 0 && value) return value;
|
|
3357
3351
|
}
|
|
3358
3352
|
const commonDir = await gitPath(worktree, "--git-common-dir");
|
|
3359
|
-
return commonDir ?
|
|
3353
|
+
return commonDir ? join17(commonDir, "hooks") : null;
|
|
3360
3354
|
}
|
|
3361
3355
|
async function removeWorktree(repo, path) {
|
|
3362
3356
|
const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
@@ -3375,7 +3369,7 @@ async function worktreeDirty(path) {
|
|
|
3375
3369
|
async function repairMovedWorktrees(targetDir) {
|
|
3376
3370
|
const subdirs = (dir) => {
|
|
3377
3371
|
try {
|
|
3378
|
-
return
|
|
3372
|
+
return readdirSync11(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join17(dir, d.name));
|
|
3379
3373
|
} catch {
|
|
3380
3374
|
return [];
|
|
3381
3375
|
}
|
|
@@ -3502,7 +3496,7 @@ __export(droneChannel_exports, {
|
|
|
3502
3496
|
verifyLocalCliToken: () => verifyLocalCliToken
|
|
3503
3497
|
});
|
|
3504
3498
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
3505
|
-
import { chmodSync, mkdirSync as
|
|
3499
|
+
import { chmodSync, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
3506
3500
|
import { dirname as dirname3 } from "path";
|
|
3507
3501
|
import { serve } from "@hono/node-server";
|
|
3508
3502
|
function loadChannelSecret(path) {
|
|
@@ -3512,7 +3506,7 @@ function loadChannelSecret(path) {
|
|
|
3512
3506
|
} catch {
|
|
3513
3507
|
}
|
|
3514
3508
|
const secret = randomBytes(32).toString("hex");
|
|
3515
|
-
|
|
3509
|
+
mkdirSync8(dirname3(path), { recursive: true });
|
|
3516
3510
|
writeFileSync10(path, secret, { mode: 384 });
|
|
3517
3511
|
try {
|
|
3518
3512
|
chmodSync(path, 384);
|
|
@@ -3857,7 +3851,7 @@ var init_droneChannel = __esm({
|
|
|
3857
3851
|
|
|
3858
3852
|
// src/attachments.ts
|
|
3859
3853
|
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
3860
|
-
import { join as
|
|
3854
|
+
import { join as join20 } from "path";
|
|
3861
3855
|
function attachmentsFromDoc(doc, origin) {
|
|
3862
3856
|
const out = [];
|
|
3863
3857
|
const walk2 = (node) => {
|
|
@@ -3948,7 +3942,7 @@ async function materializeAttachments(opts) {
|
|
|
3948
3942
|
};
|
|
3949
3943
|
const result = { files: [], skipped: [] };
|
|
3950
3944
|
if (opts.refs.length === 0) return result;
|
|
3951
|
-
const dir =
|
|
3945
|
+
const dir = join20(opts.worktree, ATTACHMENTS_DIR);
|
|
3952
3946
|
await io.removeDir(dir);
|
|
3953
3947
|
let dirReady = false;
|
|
3954
3948
|
let total = 0;
|
|
@@ -3994,7 +3988,7 @@ async function materializeAttachments(opts) {
|
|
|
3994
3988
|
await io.mkdir(dir);
|
|
3995
3989
|
dirReady = true;
|
|
3996
3990
|
}
|
|
3997
|
-
await io.writeFile(
|
|
3991
|
+
await io.writeFile(join20(dir, name), file.bytes);
|
|
3998
3992
|
} catch (e) {
|
|
3999
3993
|
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)})`);
|
|
4000
3994
|
continue;
|
|
@@ -4147,7 +4141,7 @@ var init_attachments = __esm({
|
|
|
4147
4141
|
|
|
4148
4142
|
// src/shotUpload.ts
|
|
4149
4143
|
import { readFile as fsReadFile } from "fs/promises";
|
|
4150
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
4144
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join21 } from "path";
|
|
4151
4145
|
function shotMimetype(path) {
|
|
4152
4146
|
const dot = path.lastIndexOf(".");
|
|
4153
4147
|
if (dot < 0) return null;
|
|
@@ -4173,7 +4167,7 @@ async function readShots(paths, opts) {
|
|
|
4173
4167
|
}
|
|
4174
4168
|
let bytes;
|
|
4175
4169
|
try {
|
|
4176
|
-
bytes = await read(isAbsolute3(path) ? path :
|
|
4170
|
+
bytes = await read(isAbsolute3(path) ? path : join21(opts.worktree, path));
|
|
4177
4171
|
} catch (e) {
|
|
4178
4172
|
skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
|
|
4179
4173
|
continue;
|
|
@@ -4252,7 +4246,7 @@ var init_shotUpload = __esm({
|
|
|
4252
4246
|
// src/oauth.ts
|
|
4253
4247
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
4254
4248
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
|
|
4255
|
-
import { join as
|
|
4249
|
+
import { join as join22 } from "path";
|
|
4256
4250
|
function makePkce() {
|
|
4257
4251
|
const verifier = randomBytes2(48).toString("base64url");
|
|
4258
4252
|
const challenge = createHash2("sha256").update(verifier).digest("base64url");
|
|
@@ -4387,8 +4381,8 @@ var init_oauth = __esm({
|
|
|
4387
4381
|
AUTHORIZE = "/api/oauth/authorize";
|
|
4388
4382
|
TOKEN = "/api/oauth/token";
|
|
4389
4383
|
SCOPE = "openid profile email taskless:full";
|
|
4390
|
-
STORE_PATH2 =
|
|
4391
|
-
CLIENT_PATH =
|
|
4384
|
+
STORE_PATH2 = join22(SWARM_HOME, "oauth.json");
|
|
4385
|
+
CLIENT_PATH = join22(SWARM_HOME, "oauth-client.json");
|
|
4392
4386
|
base2 = (api) => api.replace(/\/$/, "");
|
|
4393
4387
|
}
|
|
4394
4388
|
});
|
|
@@ -4435,7 +4429,7 @@ var init_authState = __esm({
|
|
|
4435
4429
|
|
|
4436
4430
|
// src/workspace.ts
|
|
4437
4431
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
|
|
4438
|
-
import { join as
|
|
4432
|
+
import { join as join23 } from "path";
|
|
4439
4433
|
function loadWorkspace() {
|
|
4440
4434
|
try {
|
|
4441
4435
|
const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
|
|
@@ -4452,7 +4446,7 @@ var init_workspace = __esm({
|
|
|
4452
4446
|
"src/workspace.ts"() {
|
|
4453
4447
|
"use strict";
|
|
4454
4448
|
init_config();
|
|
4455
|
-
WORKSPACE_PATH =
|
|
4449
|
+
WORKSPACE_PATH = join23(SWARM_HOME, "workspace.json");
|
|
4456
4450
|
}
|
|
4457
4451
|
});
|
|
4458
4452
|
|
|
@@ -4947,7 +4941,8 @@ __export(db_exports, {
|
|
|
4947
4941
|
SwarmDb: () => SwarmDb
|
|
4948
4942
|
});
|
|
4949
4943
|
import { createRequire as createRequire2 } from "module";
|
|
4950
|
-
import {
|
|
4944
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4945
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4951
4946
|
import { resolve as resolve3 } from "path";
|
|
4952
4947
|
function rowToQuestion(row) {
|
|
4953
4948
|
return {
|
|
@@ -5109,6 +5104,12 @@ CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
|
|
|
5109
5104
|
CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
|
|
5110
5105
|
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
5111
5106
|
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
|
|
5107
|
+
-- \u043A\u043B\u044E\u0447-\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u043E \u0421\u0410\u041C \u0424\u0410\u0419\u041B \u0431\u0430\u0437\u044B (\u043D\u0435 \u043F\u0440\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u044B). \u041F\u043E\u043A\u0430 \u0432 \u043D\u0451\u043C \u043E\u0434\u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C \u2014 instance_id
|
|
5108
|
+
-- (TASWM-419): \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u042D\u0422\u041E\u0419 \u0436\u0438\u0437\u043D\u0438 \u0431\u0430\u0437\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u043C \u043D\u0435\u0439\u043C\u0441\u043F\u0435\u0439\u0441\u044F\u0442\u0441\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0438 run-log'\u043E\u0432.
|
|
5109
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
5110
|
+
key TEXT PRIMARY KEY,
|
|
5111
|
+
value TEXT NOT NULL
|
|
5112
|
+
);
|
|
5112
5113
|
-- \u043C\u0430\u0440\u043A\u0435\u0440 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u044B\u0445 \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u0411\u0414 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C importDb)
|
|
5113
5114
|
CREATE TABLE IF NOT EXISTS imports (
|
|
5114
5115
|
source TEXT PRIMARY KEY,
|
|
@@ -5121,6 +5122,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5121
5122
|
defaultRepo;
|
|
5122
5123
|
databaseCtor;
|
|
5123
5124
|
// для importDb (второй Database, source)
|
|
5125
|
+
/** Идентификатор этой жизни файла базы (TASWM-419) — им неймспейсятся каталоги run-log'ов. */
|
|
5126
|
+
instanceId;
|
|
5124
5127
|
constructor(path = "swarm.db", defaultRepo = "") {
|
|
5125
5128
|
this.defaultRepo = normalizeRepo(defaultRepo);
|
|
5126
5129
|
this.databaseCtor = loadBetterSqlite3();
|
|
@@ -5188,6 +5191,22 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5188
5191
|
if (this.defaultRepo) {
|
|
5189
5192
|
this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
|
|
5190
5193
|
}
|
|
5194
|
+
this.instanceId = this.ensureInstanceId();
|
|
5195
|
+
}
|
|
5196
|
+
/**
|
|
5197
|
+
* Идентификатор ЭТОЙ жизни файла базы (TASWM-419). Заводится при первом открытии и живёт
|
|
5198
|
+
* в самой базе, поэтому переживает рестарт демона, но НЕ переживает пересоздание файла:
|
|
5199
|
+
* снесли базу — новый id, и каталоги run-log'ов нового прогона #1 не подселяются к файлам
|
|
5200
|
+
* прогона #1 из прошлой жизни. Именно это и было корнем «лог не того прогона»: номер там
|
|
5201
|
+
* был вся информация об адресе.
|
|
5202
|
+
*/
|
|
5203
|
+
ensureInstanceId() {
|
|
5204
|
+
const read = () => this.db.prepare("SELECT value FROM meta WHERE key = 'instance_id'").get()?.value;
|
|
5205
|
+
const existing = read();
|
|
5206
|
+
if (existing) return existing;
|
|
5207
|
+
const fresh = randomUUID3().replace(/-/g, "").slice(0, 12);
|
|
5208
|
+
this.db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('instance_id', ?)").run(fresh);
|
|
5209
|
+
return read() ?? fresh;
|
|
5191
5210
|
}
|
|
5192
5211
|
close() {
|
|
5193
5212
|
this.db.close();
|
|
@@ -5282,6 +5301,15 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5282
5301
|
const m = /^local:(\d+)$/.exec(key);
|
|
5283
5302
|
return m ? this.getRun(Number(m[1])) : null;
|
|
5284
5303
|
}
|
|
5304
|
+
/**
|
|
5305
|
+
* TASWM-419: адрес каталога run-log'ов — номер, НЕЙМСПЕЙСНУТЫЙ идентификатором файла базы
|
|
5306
|
+
* (`local:<instance>:<id>`). Тот же номер в пересозданной базе даёт другой instance, то
|
|
5307
|
+
* есть другой каталог: подселиться к файлам чужого прогона с тем же номером больше нечем.
|
|
5308
|
+
* Псевдонимов у sqlite-прогона нет — ключ не меняется, пока жив файл базы.
|
|
5309
|
+
*/
|
|
5310
|
+
runLogKeys(id) {
|
|
5311
|
+
return this.getRun(id) ? [`local:${this.instanceId}:${id}`] : [];
|
|
5312
|
+
}
|
|
5285
5313
|
listRuns(status) {
|
|
5286
5314
|
const rows = status ? this.db.prepare("SELECT * FROM runs WHERE status = ? ORDER BY id DESC").all(status) : this.db.prepare("SELECT * FROM runs ORDER BY id DESC").all();
|
|
5287
5315
|
return rows.map(rowToRun);
|
|
@@ -5522,7 +5550,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5522
5550
|
const repoN = normalizeRepo(repo);
|
|
5523
5551
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
5524
5552
|
if (already) return { imported: false, runs: already.run_count };
|
|
5525
|
-
if (!
|
|
5553
|
+
if (!existsSync12(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
5526
5554
|
const src = new this.databaseCtor(abs);
|
|
5527
5555
|
try {
|
|
5528
5556
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
@@ -5891,8 +5919,8 @@ var init_droneStore = __esm({
|
|
|
5891
5919
|
|
|
5892
5920
|
// src/overmind.ts
|
|
5893
5921
|
import { homedir as homedir5 } from "os";
|
|
5894
|
-
import { existsSync as
|
|
5895
|
-
import { basename as basename4, dirname as dirname4, join as
|
|
5922
|
+
import { existsSync as existsSync17, readFileSync as readFileSync20, readdirSync as readdirSync13, renameSync as renameSync11 } from "fs";
|
|
5923
|
+
import { basename as basename4, dirname as dirname4, join as join30 } from "path";
|
|
5896
5924
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
5897
5925
|
import { spawn as spawn3 } from "child_process";
|
|
5898
5926
|
|
|
@@ -5972,13 +6000,66 @@ init_config();
|
|
|
5972
6000
|
init_resources();
|
|
5973
6001
|
init_names2();
|
|
5974
6002
|
init_storm();
|
|
5975
|
-
import { appendFileSync, existsSync as
|
|
6003
|
+
import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5976
6004
|
import { dirname } from "path";
|
|
5977
6005
|
import { randomUUID } from "crypto";
|
|
5978
6006
|
|
|
5979
6007
|
// src/logTail.ts
|
|
5980
|
-
import { closeSync, existsSync as
|
|
6008
|
+
import { closeSync, existsSync as existsSync5, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
|
|
6009
|
+
import { join as join7 } from "path";
|
|
6010
|
+
|
|
6011
|
+
// src/runLogDir.ts
|
|
6012
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync3, statSync as statSync3 } from "fs";
|
|
5981
6013
|
import { join as join6 } from "path";
|
|
6014
|
+
var LEGACY_RUN_LOGS_SUBDIR = "legacy";
|
|
6015
|
+
function runLogDirName(runLogKey) {
|
|
6016
|
+
const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
|
|
6017
|
+
if (!safe2) throw new Error(`\u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u0433\u043E\u0434\u0438\u0442\u0441\u044F \u0432 \u0438\u043C\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 run-log'\u043E\u0432: ${JSON.stringify(runLogKey)}`);
|
|
6018
|
+
return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
|
|
6019
|
+
}
|
|
6020
|
+
function runLogDirPath(runLogsDir, runLogKey) {
|
|
6021
|
+
return join6(runLogsDir, runLogDirName(runLogKey));
|
|
6022
|
+
}
|
|
6023
|
+
function findRunLogDir(runLogsDir, keys) {
|
|
6024
|
+
for (const key of keys) {
|
|
6025
|
+
const dir = runLogDirPath(runLogsDir, key);
|
|
6026
|
+
if (existsSync4(dir)) return dir;
|
|
6027
|
+
}
|
|
6028
|
+
return null;
|
|
6029
|
+
}
|
|
6030
|
+
function runLogDirForWrite(runLogsDir, keys) {
|
|
6031
|
+
const canonical = keys[0];
|
|
6032
|
+
if (!canonical) return null;
|
|
6033
|
+
return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
|
|
6034
|
+
}
|
|
6035
|
+
function sweepLegacyRunLogs(runLogsDir) {
|
|
6036
|
+
const legacyDir = join6(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
|
|
6037
|
+
let entries;
|
|
6038
|
+
try {
|
|
6039
|
+
entries = readdirSync4(runLogsDir);
|
|
6040
|
+
} catch {
|
|
6041
|
+
return { moved: 0, failed: 0, legacyDir };
|
|
6042
|
+
}
|
|
6043
|
+
let moved = 0;
|
|
6044
|
+
let failed = 0;
|
|
6045
|
+
for (const name of entries) {
|
|
6046
|
+
if (!/^\d+$/.test(name)) continue;
|
|
6047
|
+
const from = join6(runLogsDir, name);
|
|
6048
|
+
try {
|
|
6049
|
+
if (!statSync3(from).isDirectory()) continue;
|
|
6050
|
+
mkdirSync4(legacyDir, { recursive: true });
|
|
6051
|
+
let to = join6(legacyDir, name);
|
|
6052
|
+
for (let n = 2; existsSync4(to) && n < 100; n++) to = join6(legacyDir, `${name}-${n}`);
|
|
6053
|
+
renameSync3(from, to);
|
|
6054
|
+
moved++;
|
|
6055
|
+
} catch {
|
|
6056
|
+
failed++;
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
return { moved, failed, legacyDir };
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
// src/logTail.ts
|
|
5982
6063
|
var TAIL_LINES_FALLBACK = 200;
|
|
5983
6064
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
5984
6065
|
function fileHeader(name, size, shown) {
|
|
@@ -5988,7 +6069,7 @@ function fileHeader(name, size, shown) {
|
|
|
5988
6069
|
function readTail(path, opts) {
|
|
5989
6070
|
let size;
|
|
5990
6071
|
try {
|
|
5991
|
-
size =
|
|
6072
|
+
size = statSync4(path).size;
|
|
5992
6073
|
} catch {
|
|
5993
6074
|
return null;
|
|
5994
6075
|
}
|
|
@@ -6018,11 +6099,12 @@ function readTail(path, opts) {
|
|
|
6018
6099
|
text = rows.join("\n");
|
|
6019
6100
|
return { text, size, shown: Buffer.byteLength(text, "utf8") };
|
|
6020
6101
|
}
|
|
6021
|
-
function latestRunLaunch(runLogsDir,
|
|
6022
|
-
const dir =
|
|
6102
|
+
function latestRunLaunch(runLogsDir, runLogKeys) {
|
|
6103
|
+
const dir = findRunLogDir(runLogsDir, runLogKeys);
|
|
6104
|
+
if (!dir) return null;
|
|
6023
6105
|
let files;
|
|
6024
6106
|
try {
|
|
6025
|
-
files =
|
|
6107
|
+
files = readdirSync5(dir);
|
|
6026
6108
|
} catch {
|
|
6027
6109
|
return null;
|
|
6028
6110
|
}
|
|
@@ -6033,13 +6115,17 @@ function latestRunLaunch(runLogsDir, localRunId) {
|
|
|
6033
6115
|
}
|
|
6034
6116
|
if (indexes.size === 0) return null;
|
|
6035
6117
|
const index = Math.max(...indexes);
|
|
6036
|
-
const out = [
|
|
6037
|
-
const err =
|
|
6118
|
+
const out = [join7(dir, `${index}.json`), join7(dir, `${index}.json.done`)].find((p) => existsSync5(p)) ?? null;
|
|
6119
|
+
const err = existsSync5(join7(dir, `${index}.err.log`)) ? join7(dir, `${index}.err.log`) : null;
|
|
6038
6120
|
return { index, total: indexes.size, out, err };
|
|
6039
6121
|
}
|
|
6040
|
-
function runLogTail(runLogsDir,
|
|
6041
|
-
const launch = latestRunLaunch(runLogsDir,
|
|
6042
|
-
if (!launch)
|
|
6122
|
+
function runLogTail(runLogsDir, runLogKeys, opts) {
|
|
6123
|
+
const launch = latestRunLaunch(runLogsDir, runLogKeys);
|
|
6124
|
+
if (!launch) {
|
|
6125
|
+
const canonical = runLogKeys[0];
|
|
6126
|
+
const where = canonical ? runLogDirPath(runLogsDir, canonical) : runLogsDir;
|
|
6127
|
+
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 (${where} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
|
|
6128
|
+
}
|
|
6043
6129
|
const parts = [];
|
|
6044
6130
|
let budget = opts.maxBytes;
|
|
6045
6131
|
if (launch.err) {
|
|
@@ -6094,8 +6180,8 @@ async function serveLogRequests(deps, commands) {
|
|
|
6094
6180
|
} else if (!cmd.runId) {
|
|
6095
6181
|
result = { error: "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0431\u0435\u0437 runId \u2014 \u043D\u0435\u0447\u0435\u0433\u043E \u0447\u0438\u0442\u0430\u0442\u044C" };
|
|
6096
6182
|
} else {
|
|
6097
|
-
const
|
|
6098
|
-
result =
|
|
6183
|
+
const keys = deps.runLogKeys(cmd.runId);
|
|
6184
|
+
result = keys.length === 0 ? { error: `\u043F\u0440\u043E\u0433\u043E\u043D ${cmd.runId} \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0435\u0433\u043E \u0432\u0435\u043B\u043E \u043D\u0435 \u043E\u043D\u0430?)` } : runLogTail(deps.runLogsDir, keys, opts);
|
|
6099
6185
|
}
|
|
6100
6186
|
} catch (e) {
|
|
6101
6187
|
result = { error: `\u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u043B\u043E\u0433: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -6191,6 +6277,11 @@ var SwarmClient = class _SwarmClient {
|
|
|
6191
6277
|
// Живёт в памяти: рестарт демона просто переспросит прод.
|
|
6192
6278
|
depsProd = /* @__PURE__ */ new Map();
|
|
6193
6279
|
depsAskedAt = /* @__PURE__ */ new Map();
|
|
6280
|
+
// TASWM-399: ссылка dependsOn → человеко-слаг блокера, как его назвал прод. В колонке
|
|
6281
|
+
// лежит cuid прогона или задачи, а в лог демона, в событие прогона и в аномалию должен
|
|
6282
|
+
// уходить «TASWM-392», а не «cmsqqpphd011y2xtbckkut6bj» — иначе оператору всё равно
|
|
6283
|
+
// нечего искать. Резолв делает прод (см. resolveDepRefs), здесь только запоминаем.
|
|
6284
|
+
depSlug = /* @__PURE__ */ new Map();
|
|
6194
6285
|
static DEPS_TTL_MS = 6e4;
|
|
6195
6286
|
// TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
|
|
6196
6287
|
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
@@ -6250,25 +6341,25 @@ var SwarmClient = class _SwarmClient {
|
|
|
6250
6341
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
6251
6342
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
6252
6343
|
this.host = opts.host ?? null;
|
|
6253
|
-
this.repoExists = opts.repoExists ??
|
|
6344
|
+
this.repoExists = opts.repoExists ?? existsSync6;
|
|
6254
6345
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
6255
6346
|
this.executorId = opts.executorId ?? EXECUTOR.id;
|
|
6256
6347
|
this.outboxPath = opts.outboxPath;
|
|
6257
6348
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
6258
6349
|
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
6259
|
-
|
|
6350
|
+
mkdirSync5(dirname(this.outboxPath), { recursive: true });
|
|
6260
6351
|
this.recover();
|
|
6261
6352
|
}
|
|
6262
6353
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
6263
6354
|
recover() {
|
|
6264
|
-
if (
|
|
6355
|
+
if (existsSync6(this.mapPath)) {
|
|
6265
6356
|
try {
|
|
6266
6357
|
const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
|
|
6267
6358
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
6268
6359
|
} catch {
|
|
6269
6360
|
}
|
|
6270
6361
|
}
|
|
6271
|
-
if (
|
|
6362
|
+
if (existsSync6(this.outboxPath)) {
|
|
6272
6363
|
const txt = readFileSync4(this.outboxPath, "utf8");
|
|
6273
6364
|
for (const line of txt.split("\n")) {
|
|
6274
6365
|
const s = line.trim();
|
|
@@ -6289,7 +6380,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
6289
6380
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
6290
6381
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
6291
6382
|
replayGlobalEvents() {
|
|
6292
|
-
if (!
|
|
6383
|
+
if (!existsSync6(this.stormPath)) return;
|
|
6293
6384
|
let txt;
|
|
6294
6385
|
try {
|
|
6295
6386
|
txt = readFileSync4(this.stormPath, "utf8");
|
|
@@ -6554,6 +6645,24 @@ var SwarmClient = class _SwarmClient {
|
|
|
6554
6645
|
const viaProd = [...this.runs.values()].find((r) => r.runKey === `p:${prodId}` || this.prodIdByKey.get(r.runKey) === prodId);
|
|
6555
6646
|
return viaProd ? this.mat(viaProd) : null;
|
|
6556
6647
|
}
|
|
6648
|
+
/**
|
|
6649
|
+
* TASWM-419: адреса каталога run-log'ов прогона. КАНОНИЧЕСКИЙ — прод-вида `p:<cuid>`, как
|
|
6650
|
+
* только cuid известен: он один и тот же во всех жизнях демона, а локальный uuid прогона
|
|
6651
|
+
* после рестарта уже не воскресает (гидрированный прогон приезжает под прод-ключом).
|
|
6652
|
+
* Дальше — псевдонимы: uuid, под которым каталог мог завестись в окно между спавном и
|
|
6653
|
+
* первым флашем в прод. По ним каталог ищется, чтобы запуски одного прогона не расползлись
|
|
6654
|
+
* по двум каталогам (иначе cost недосчитал бы часть, а хвост показал бы не тот запуск).
|
|
6655
|
+
* Номер прогона в адресе не участвует вовсе — он и есть источник «лога чужого прогона».
|
|
6656
|
+
*/
|
|
6657
|
+
runLogKeys(id) {
|
|
6658
|
+
const key = this.runs.get(id)?.runKey;
|
|
6659
|
+
if (!key) return [];
|
|
6660
|
+
const prodId = key.startsWith("p:") ? key.slice(2) : this.prodIdByKey.get(key);
|
|
6661
|
+
if (!prodId) return [key];
|
|
6662
|
+
const canonical = `p:${prodId}`;
|
|
6663
|
+
const aliases = [key, ...[...this.prodIdByKey.entries()].filter(([, v]) => v === prodId).map(([k]) => k)];
|
|
6664
|
+
return [canonical, ...new Set(aliases.filter((k) => k !== canonical))];
|
|
6665
|
+
}
|
|
6557
6666
|
listRuns(status) {
|
|
6558
6667
|
const all = [...this.runs.values()].filter((r) => status ? r.status === status : true);
|
|
6559
6668
|
all.sort((a, b) => b.id - a.id);
|
|
@@ -6670,16 +6779,25 @@ var SwarmClient = class _SwarmClient {
|
|
|
6670
6779
|
/**
|
|
6671
6780
|
* TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
|
|
6672
6781
|
*
|
|
6673
|
-
*
|
|
6674
|
-
* (
|
|
6675
|
-
* стоят разного:
|
|
6676
|
-
* • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он
|
|
6677
|
-
* портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
|
|
6678
|
-
* •
|
|
6679
|
-
* на соседней машине как раз и может быть
|
|
6680
|
-
*
|
|
6681
|
-
*
|
|
6682
|
-
*
|
|
6782
|
+
* ИСТОЧНИК ПРАВДЫ — ПРОД (TASWM-399), зеркало здесь только быстрый путь. Зеркало —
|
|
6783
|
+
* ЧАСТИЧНАЯ выборка (прогоны этой машины, окно гидрации), и по частичной выборке два
|
|
6784
|
+
* вывода стоят разного:
|
|
6785
|
+
* • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он
|
|
6786
|
+
* не портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
|
|
6787
|
+
* • всё остальное — вывод от ОТСУТСТВИЯ, и его принимаем ТОЛЬКО из ответа прода
|
|
6788
|
+
* (refreshDeps): невидимый зеркалу прогон на соседней машине как раз и может быть
|
|
6789
|
+
* живым, а закрытый с прода — давно смерженным.
|
|
6790
|
+
*
|
|
6791
|
+
* ССЫЛКА — НЕ ОБЯЗАТЕЛЬНО СЛАГ. В `dependsOn` лежит чаще всего cuid ПРОГОНА (так пишет
|
|
6792
|
+
* overmind), реже cuid задачи, и только дев-спавн `--after` пишет человеко-слаг. Быстрый
|
|
6793
|
+
* путь по зеркалу сравнивает `task_slug`, то есть срабатывает лишь на третьей форме —
|
|
6794
|
+
* это нормально: остальные две резолвит прод. А вот считать «раз в зеркале не нашёл —
|
|
6795
|
+
* значит не готово» было корнем TASWM-399.
|
|
6796
|
+
*
|
|
6797
|
+
* Прода ещё не спрашивали (первый тик) или он моргнул → `pending`: прогон честно ждёт
|
|
6798
|
+
* следующего окна. Лишняя минута очереди дешевле дрона, поднятого поверх работающего
|
|
6799
|
+
* блокера. `unknown` (блокера НЕ ЗНАЕТ И ПРОД) ставит только ответ прода — местным
|
|
6800
|
+
* незнанием такой вывод не делается.
|
|
6683
6801
|
*/
|
|
6684
6802
|
depVerdicts(depends_on) {
|
|
6685
6803
|
if (!depends_on || depends_on.length === 0) return [];
|
|
@@ -6689,57 +6807,89 @@ var SwarmClient = class _SwarmClient {
|
|
|
6689
6807
|
doneInMirror.add(r.task_slug);
|
|
6690
6808
|
}
|
|
6691
6809
|
}
|
|
6692
|
-
return depends_on.map((
|
|
6693
|
-
if (doneInMirror.has(
|
|
6694
|
-
return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
|
|
6810
|
+
return depends_on.map((ref) => {
|
|
6811
|
+
if (doneInMirror.has(ref)) {
|
|
6812
|
+
return { slug: ref, outcome: "done", met: true, status: "done", name: null, stopReason: null };
|
|
6813
|
+
}
|
|
6814
|
+
const fromProd = this.depsProd.get(ref);
|
|
6815
|
+
if (fromProd) {
|
|
6816
|
+
if (fromProd.outcome === "unknown" && this.knowsDepRef(ref)) {
|
|
6817
|
+
return { slug: this.depSlug.get(ref) ?? ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
|
|
6818
|
+
}
|
|
6819
|
+
return { slug: this.depSlug.get(ref) ?? ref, ...fromProd };
|
|
6695
6820
|
}
|
|
6696
|
-
|
|
6697
|
-
if (fromProd) return { slug, ...fromProd };
|
|
6698
|
-
return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
|
|
6821
|
+
return { slug: ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
|
|
6699
6822
|
});
|
|
6700
6823
|
}
|
|
6701
6824
|
/**
|
|
6702
|
-
*
|
|
6825
|
+
* Знает ли ЗЕРКАЛО хоть один прогон по ссылке `dependsOn` (TASWM-399). Ссылка бывает
|
|
6826
|
+
* слагом, cuid'ом задачи и cuid'ом прогона — последний сверяем с прод-id, как это
|
|
6827
|
+
* делают runByProdId/refreshRun. Служебные сабраны (ревьюер/резолвер/верификатор) не
|
|
6828
|
+
* в счёт: их существование не означает, что жив сам блокер.
|
|
6829
|
+
*/
|
|
6830
|
+
knowsDepRef(ref) {
|
|
6831
|
+
for (const r of this.runs.values()) {
|
|
6832
|
+
if (r.resolves_run_id != null || r.reviews_run_id != null || r.verifies_run_id != null) continue;
|
|
6833
|
+
if (r.task_slug === ref) return true;
|
|
6834
|
+
if (r.runKey === `p:${ref}` || this.prodIdByKey.get(r.runKey) === ref) return true;
|
|
6835
|
+
}
|
|
6836
|
+
return false;
|
|
6837
|
+
}
|
|
6838
|
+
/**
|
|
6839
|
+
* TASWM-300 → TASWM-399: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
|
|
6840
|
+
*
|
|
6841
|
+
* Почему зеркала мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine, hydrateRun) и
|
|
6842
|
+
* только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка по updatedAt
|
|
6843
|
+
* desc — сутки работы роя). Зависимость, закрытая на соседней машине, обнулившаяся при
|
|
6844
|
+
* рестарте демона или просто позавчерашняя, в зеркало не попадала — и `dependsOn` держал
|
|
6845
|
+
* прогон в очереди вечно.
|
|
6703
6846
|
*
|
|
6704
|
-
*
|
|
6705
|
-
*
|
|
6706
|
-
*
|
|
6707
|
-
*
|
|
6708
|
-
* (
|
|
6847
|
+
* ЧЕМ СПРАШИВАЕМ И ПОЧЕМУ НЕ `swarm_list_runs` (корень TASWM-399). До этой задачи здесь
|
|
6848
|
+
* шёл `swarm_list_runs { taskId: <ref> }` — то есть ссылка трактовалась как ЗАДАЧА. Но в
|
|
6849
|
+
* `dependsOn` чаще всего лежит cuid ПРОГОНА: `resolveTaskRef` его задачей не признаёт,
|
|
6850
|
+
* ответ приходит `isError`, вердикт не появляется НИКОГДА, и прогон стоит молча при
|
|
6851
|
+
* свободной машине (12–13.08 — пять остановок роя за сутки). `swarm_deps_status` понимает
|
|
6852
|
+
* все три формы ссылки и отвечает по единому правилу роя (`judgeDep` на прод-стороне).
|
|
6709
6853
|
*
|
|
6710
6854
|
* Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
|
|
6711
6855
|
* положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
|
|
6712
|
-
* «исчерпана
|
|
6713
|
-
*
|
|
6714
|
-
*
|
|
6856
|
+
* «исчерпана неуспешно» и «блокера не знает никто») переспрашиваем не чаще DEPS_TTL_MS:
|
|
6857
|
+
* оператор вправе переиграть блокер новым прогоном, а тик идёт раз в ~4 с — без TTL это
|
|
6858
|
+
* был бы поход в прод на каждую ссылку каждый тик.
|
|
6859
|
+
*
|
|
6860
|
+
* Одним вызовом на все просроченные ссылки, а не по вызову на ссылку: очередь ждущих
|
|
6861
|
+
* бывает в десятки прогонов, и N запросов на тик — это тот же самый поход в прод, только
|
|
6862
|
+
* дороже.
|
|
6715
6863
|
*/
|
|
6716
|
-
async refreshDeps(
|
|
6717
|
-
|
|
6718
|
-
if (!
|
|
6719
|
-
const asked = this.depsAskedAt.get(
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6864
|
+
async refreshDeps(refs, nowMs = Date.now()) {
|
|
6865
|
+
const ask = [...new Set(refs)].filter((ref) => {
|
|
6866
|
+
if (!ref || this.depsProd.get(ref)?.outcome === "done") return false;
|
|
6867
|
+
const asked = this.depsAskedAt.get(ref);
|
|
6868
|
+
return asked === void 0 || nowMs - asked >= _SwarmClient.DEPS_TTL_MS;
|
|
6869
|
+
});
|
|
6870
|
+
if (!ask.length) return;
|
|
6871
|
+
for (const ref of ask) this.depsAskedAt.set(ref, nowMs);
|
|
6872
|
+
let res;
|
|
6873
|
+
try {
|
|
6874
|
+
res = await this.call("swarm_deps_status", { refs: ask });
|
|
6875
|
+
} catch {
|
|
6876
|
+
for (const ref of ask) this.depsAskedAt.delete(ref);
|
|
6877
|
+
return;
|
|
6878
|
+
}
|
|
6879
|
+
if (res?.isError) return;
|
|
6880
|
+
const deps = res.structuredContent?.deps ?? [];
|
|
6881
|
+
for (const d of deps) {
|
|
6882
|
+
const ref = typeof d.ref === "string" ? d.ref : null;
|
|
6883
|
+
if (!ref) continue;
|
|
6884
|
+
const outcome = String(d.outcome ?? "pending");
|
|
6885
|
+
this.depsProd.set(ref, {
|
|
6886
|
+
outcome,
|
|
6887
|
+
met: Boolean(d.met),
|
|
6888
|
+
status: d.status ?? null,
|
|
6889
|
+
name: d.name ?? null,
|
|
6890
|
+
stopReason: d.stopReason ?? null
|
|
6891
|
+
});
|
|
6892
|
+
if (typeof d.slug === "string" && d.slug) this.depSlug.set(ref, d.slug);
|
|
6743
6893
|
}
|
|
6744
6894
|
}
|
|
6745
6895
|
/**
|
|
@@ -7058,7 +7208,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7058
7208
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
7059
7209
|
*/
|
|
7060
7210
|
reconcileRuntime(opts) {
|
|
7061
|
-
const wtExists = opts.worktreeExists ??
|
|
7211
|
+
const wtExists = opts.worktreeExists ?? existsSync6;
|
|
7062
7212
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
7063
7213
|
try {
|
|
7064
7214
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -7547,6 +7697,12 @@ var SwarmClient = class _SwarmClient {
|
|
|
7547
7697
|
...typeof h.paused === "boolean" ? { paused: h.paused } : {},
|
|
7548
7698
|
...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
|
|
7549
7699
|
...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
|
|
7700
|
+
// TASWM-424: «доступ отозван» — состояние ровно того же рода, что пауза, и
|
|
7701
|
+
// формулировка у него ОДНА на все поверхности (HOST_ACCESS_REVOKED_TEXT):
|
|
7702
|
+
// `hostLines` зовёт тот же `hostStateText`, что и парк машин в /swarm. Без
|
|
7703
|
+
// этого поля `swarm hosts` показывал отвергнутую машину просто молчащей —
|
|
7704
|
+
// то самое различие, ради которого флаг и заводили (TASWM-324).
|
|
7705
|
+
...typeof h.accessRevoked === "boolean" ? { accessRevoked: h.accessRevoked } : {},
|
|
7550
7706
|
// Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
|
|
7551
7707
|
// ровно как с паузой выше: «не знаем» и так значит «не знаем».
|
|
7552
7708
|
...resources ? { resources } : {}
|
|
@@ -7632,9 +7788,18 @@ var SwarmClient = class _SwarmClient {
|
|
|
7632
7788
|
* ВСЕГДА (в т.ч. `storm:false`), как паузу: это состояние, а не команда, и молчание тут
|
|
7633
7789
|
* означало бы «прод помнит вчерашний шторм», когда рой давно работает.
|
|
7634
7790
|
*
|
|
7791
|
+
* TASWM-402: тем же пульсом уезжают КОРНИ WORKTREE'ЕВ этой машины (`worktreeRoots` —
|
|
7792
|
+
* `owner/repo` → `<worktreesDir>/<repo-basename>`). Это НЕ рантайм и правило «pid и путь
|
|
7793
|
+
* дрона в прод не уходят» не трогает: корень — статичная конфигурация машины, того же
|
|
7794
|
+
* рода, что реестр репо и ёмкость, он не зависит ни от прогона, ни от живости процесса и
|
|
7795
|
+
* не протухает между пульсами. Прод склеивает `<корень>/<слаг>` сам — и отвечает человеку
|
|
7796
|
+
* на вопрос «куда идти руками», когда команда машине не доиграла (а в этом случае, по
|
|
7797
|
+
* определению, спросить рантайм у молчащей машины уже нельзя). Не передали корень (не
|
|
7798
|
+
* знаем своего каталога) → ключа нет, прод прошлое не трогает — как с capacity/resources.
|
|
7799
|
+
*
|
|
7635
7800
|
* Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
|
|
7636
7801
|
*/
|
|
7637
|
-
async heartbeat(daemonVersion, pause, capacity, resources, storm, signIn) {
|
|
7802
|
+
async heartbeat(daemonVersion, pause, capacity, resources, storm, worktreesDir, signIn) {
|
|
7638
7803
|
try {
|
|
7639
7804
|
const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
|
|
7640
7805
|
if (daemonVersion) args.daemonVersion = daemonVersion;
|
|
@@ -7652,6 +7817,11 @@ var SwarmClient = class _SwarmClient {
|
|
|
7652
7817
|
args.storm = storm.active;
|
|
7653
7818
|
if (storm.reason) args.stormReason = storm.reason;
|
|
7654
7819
|
}
|
|
7820
|
+
if (worktreesDir) {
|
|
7821
|
+
const roots = {};
|
|
7822
|
+
for (const [full, path] of this.repoPathByFullName) roots[full] = droneWorktreeRoot(worktreesDir, path);
|
|
7823
|
+
if (Object.keys(roots).length) args.worktreeRoots = roots;
|
|
7824
|
+
}
|
|
7655
7825
|
if (this.host) {
|
|
7656
7826
|
args.hostId = this.host.hostId;
|
|
7657
7827
|
args.hostName = this.host.hostName;
|
|
@@ -7800,10 +7970,20 @@ var SwarmClient = class _SwarmClient {
|
|
|
7800
7970
|
* именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
|
|
7801
7971
|
* repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
|
|
7802
7972
|
* (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
|
|
7973
|
+
*
|
|
7974
|
+
* РЕГИСТР НЕ РАЗЛИЧАЕМ (TASWM-422). Ключи карты приведены к нижнему регистру
|
|
7975
|
+
* (parseRepoFullName — «GitHub сам не различает case в owner/repo»), а прод присылает
|
|
7976
|
+
* `run.repo` тем регистром, каким репозиторий записан в проекте. 17.08 это разошлось:
|
|
7977
|
+
* машина объявляла `ilyasmurov/ansamblist`, задача пришла с `ilyasmurov/Ansamblist`,
|
|
7978
|
+
* `Map.get` промахнулся — и вместо пути в прогон уехало полное имя репозитория. Дальше
|
|
7979
|
+
* git-операции шли в несуществующий каталог, прогон умирал за 5 секунд, а задача
|
|
7980
|
+
* возвращалась в очередь и бралась снова: пять павших прогонов за пять минут.
|
|
7981
|
+
* Возвращаем ИСХОДНУЮ строку, а не приведённую: промах бывает и на локальном пути
|
|
7982
|
+
* (ручной спавн), а путь регистр различает — вернув lowercase, мы сломали бы его.
|
|
7803
7983
|
*/
|
|
7804
7984
|
resolveRepo(raw) {
|
|
7805
7985
|
if (!raw) return "";
|
|
7806
|
-
return this.repoPathByFullName.get(raw) ?? raw;
|
|
7986
|
+
return this.repoPathByFullName.get(raw) ?? this.repoPathByFullName.get(raw.toLowerCase()) ?? raw;
|
|
7807
7987
|
}
|
|
7808
7988
|
/**
|
|
7809
7989
|
* TASWM-281: МОЯ ли это работа. Три исхода:
|
|
@@ -8244,20 +8424,20 @@ init_config();
|
|
|
8244
8424
|
init_heartbeat();
|
|
8245
8425
|
|
|
8246
8426
|
// src/logRotate.ts
|
|
8247
|
-
import { copyFileSync, renameSync as
|
|
8427
|
+
import { copyFileSync, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
|
|
8248
8428
|
var realIo = {
|
|
8249
8429
|
size: (path) => {
|
|
8250
8430
|
try {
|
|
8251
|
-
return
|
|
8431
|
+
return statSync5(path).size;
|
|
8252
8432
|
} catch {
|
|
8253
8433
|
return null;
|
|
8254
8434
|
}
|
|
8255
8435
|
},
|
|
8256
8436
|
copy: copyFileSync,
|
|
8257
|
-
rename:
|
|
8437
|
+
rename: renameSync5,
|
|
8258
8438
|
truncate: (path) => truncateSync(path, 0),
|
|
8259
8439
|
unlink: unlinkSync2,
|
|
8260
|
-
exists:
|
|
8440
|
+
exists: existsSync7
|
|
8261
8441
|
};
|
|
8262
8442
|
var realOwnerIo = {
|
|
8263
8443
|
statFd: (fd) => {
|
|
@@ -8270,7 +8450,7 @@ var realOwnerIo = {
|
|
|
8270
8450
|
},
|
|
8271
8451
|
statPath: (path) => {
|
|
8272
8452
|
try {
|
|
8273
|
-
const s =
|
|
8453
|
+
const s = statSync5(path);
|
|
8274
8454
|
return { dev: s.dev, ino: s.ino };
|
|
8275
8455
|
} catch {
|
|
8276
8456
|
return null;
|
|
@@ -8342,12 +8522,12 @@ async function buildRepoRegistry(paths) {
|
|
|
8342
8522
|
|
|
8343
8523
|
// src/repoMemory.ts
|
|
8344
8524
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
8345
|
-
import { join as
|
|
8525
|
+
import { join as join8 } from "path";
|
|
8346
8526
|
var FILE = "repos.json";
|
|
8347
8527
|
function loadRememberedRepos(home) {
|
|
8348
8528
|
let raw;
|
|
8349
8529
|
try {
|
|
8350
|
-
raw = readFileSync6(
|
|
8530
|
+
raw = readFileSync6(join8(home, FILE), "utf8");
|
|
8351
8531
|
} catch {
|
|
8352
8532
|
return [];
|
|
8353
8533
|
}
|
|
@@ -8362,7 +8542,7 @@ function loadRememberedRepos(home) {
|
|
|
8362
8542
|
function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
|
|
8363
8543
|
const clean = paths.filter((p) => p.trim().length > 0);
|
|
8364
8544
|
if (clean.length === 0) return;
|
|
8365
|
-
writeFileSync6(
|
|
8545
|
+
writeFileSync6(join8(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
|
|
8366
8546
|
`);
|
|
8367
8547
|
}
|
|
8368
8548
|
function pickRepoPaths(args) {
|
|
@@ -8377,12 +8557,12 @@ init_src();
|
|
|
8377
8557
|
init_config();
|
|
8378
8558
|
init_linkFailure();
|
|
8379
8559
|
import { homedir as homedir3 } from "os";
|
|
8380
|
-
import { readFileSync as readFileSync9, renameSync as
|
|
8381
|
-
import { isAbsolute, join as
|
|
8560
|
+
import { readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
8561
|
+
import { isAbsolute, join as join11, resolve as resolve2 } from "path";
|
|
8382
8562
|
|
|
8383
8563
|
// src/repoScan.ts
|
|
8384
|
-
import { readdirSync as
|
|
8385
|
-
import { join as
|
|
8564
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
|
|
8565
|
+
import { join as join10 } from "path";
|
|
8386
8566
|
var SKIP = /* @__PURE__ */ new Set([
|
|
8387
8567
|
"node_modules",
|
|
8388
8568
|
"vendor",
|
|
@@ -8412,7 +8592,7 @@ function parseOriginFromGitConfig(text) {
|
|
|
8412
8592
|
}
|
|
8413
8593
|
function originOf(dir) {
|
|
8414
8594
|
try {
|
|
8415
|
-
return parseOriginFromGitConfig(readFileSync8(
|
|
8595
|
+
return parseOriginFromGitConfig(readFileSync8(join10(dir, ".git", "config"), "utf8"));
|
|
8416
8596
|
} catch {
|
|
8417
8597
|
return null;
|
|
8418
8598
|
}
|
|
@@ -8441,15 +8621,15 @@ function walk(dir, depth, maxDepth, found) {
|
|
|
8441
8621
|
}
|
|
8442
8622
|
let entries;
|
|
8443
8623
|
try {
|
|
8444
|
-
entries =
|
|
8624
|
+
entries = readdirSync6(dir);
|
|
8445
8625
|
} catch {
|
|
8446
8626
|
return;
|
|
8447
8627
|
}
|
|
8448
8628
|
for (const name of entries) {
|
|
8449
8629
|
if (name.startsWith(".") || SKIP.has(name)) continue;
|
|
8450
|
-
const child =
|
|
8630
|
+
const child = join10(dir, name);
|
|
8451
8631
|
try {
|
|
8452
|
-
if (!
|
|
8632
|
+
if (!statSync6(child).isDirectory()) continue;
|
|
8453
8633
|
} catch {
|
|
8454
8634
|
continue;
|
|
8455
8635
|
}
|
|
@@ -8469,7 +8649,7 @@ function defaultScanRoots(home, knownPaths = []) {
|
|
|
8469
8649
|
if (parent && parent !== p) add(parent);
|
|
8470
8650
|
}
|
|
8471
8651
|
for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
|
|
8472
|
-
add(
|
|
8652
|
+
add(join10(home, name));
|
|
8473
8653
|
}
|
|
8474
8654
|
return out;
|
|
8475
8655
|
}
|
|
@@ -8483,7 +8663,7 @@ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
|
|
|
8483
8663
|
for (const part of v.split(",")) {
|
|
8484
8664
|
const p = part.trim();
|
|
8485
8665
|
if (!p) continue;
|
|
8486
|
-
const abs = p === "~" ? home : p.startsWith("~/") ?
|
|
8666
|
+
const abs = p === "~" ? home : p.startsWith("~/") ? join11(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
|
|
8487
8667
|
if (!out.includes(abs)) out.push(abs);
|
|
8488
8668
|
}
|
|
8489
8669
|
return out;
|
|
@@ -8548,6 +8728,28 @@ function describeReposFailure(kind, detail) {
|
|
|
8548
8728
|
function emptyRepoMapState(roots) {
|
|
8549
8729
|
return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
|
|
8550
8730
|
}
|
|
8731
|
+
var SCAN_REASON_TEXT = {
|
|
8732
|
+
"list-unknown": "\u043F\u0440\u043E\u0434 \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F \u2014 \u0441\u043C\u043E\u0442\u0440\u044E \u0441\u0430\u043C, \u0447\u0435\u0433\u043E \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u043F\u0440\u0438\u0431\u0430\u0432\u0438\u043B\u043E\u0441\u044C",
|
|
8733
|
+
"wanted-missing": "\u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430 \u0440\u043E\u044F \u0447\u0435\u0433\u043E-\u0442\u043E \u043D\u0435\u0442 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435",
|
|
8734
|
+
"cache-expired": "\u043A\u044D\u0448 \u043A\u0430\u0440\u0442\u044B \u043F\u0440\u043E\u0442\u0443\u0445"
|
|
8735
|
+
};
|
|
8736
|
+
var REPO_SCAN_TTL_MS = 24 * 60 * 6e4;
|
|
8737
|
+
var UNKNOWN_LIST_SCAN_TTL_MS = 60 * 6e4;
|
|
8738
|
+
function isScanStale(scannedAt, ttlMs, now = Date.now()) {
|
|
8739
|
+
if (!scannedAt) return true;
|
|
8740
|
+
const t = stampMs(scannedAt);
|
|
8741
|
+
if (!Number.isFinite(t)) return true;
|
|
8742
|
+
return now - t >= ttlMs;
|
|
8743
|
+
}
|
|
8744
|
+
function decideScan(opts) {
|
|
8745
|
+
if (!opts.known) {
|
|
8746
|
+
const ttl = Math.min(opts.ttlMs, UNKNOWN_LIST_SCAN_TTL_MS);
|
|
8747
|
+
return isScanStale(opts.scannedAt, ttl, opts.now) ? { scan: true, reason: "list-unknown" } : { scan: false, reason: null };
|
|
8748
|
+
}
|
|
8749
|
+
if (opts.missingWanted > 0) return { scan: true, reason: "wanted-missing" };
|
|
8750
|
+
if (isScanStale(opts.scannedAt, opts.ttlMs, opts.now)) return { scan: true, reason: "cache-expired" };
|
|
8751
|
+
return { scan: false, reason: null };
|
|
8752
|
+
}
|
|
8551
8753
|
async function refreshRepoMap(opts) {
|
|
8552
8754
|
const log = opts.log ?? (() => {
|
|
8553
8755
|
});
|
|
@@ -8561,14 +8763,21 @@ async function refreshRepoMap(opts) {
|
|
|
8561
8763
|
clones.set(repoFullName, { repoFullName, path, source, alternatives });
|
|
8562
8764
|
};
|
|
8563
8765
|
for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
|
|
8564
|
-
|
|
8766
|
+
const cache = loadRepoMapCache(opts.cachePath);
|
|
8767
|
+
for (const e of cache.entries) {
|
|
8565
8768
|
const name = e.repoFullName.toLowerCase();
|
|
8566
8769
|
if (clones.has(name)) continue;
|
|
8567
8770
|
if (repoAt(e.path) === name) take(name, e.path, "cache");
|
|
8568
8771
|
}
|
|
8569
8772
|
const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
|
|
8570
|
-
const
|
|
8571
|
-
|
|
8773
|
+
const decision = decideScan({
|
|
8774
|
+
known: !!wanted,
|
|
8775
|
+
missingWanted: wantedNames.filter((n) => !clones.has(n)).length,
|
|
8776
|
+
scannedAt: cache.scannedAt,
|
|
8777
|
+
ttlMs: opts.scanTtlMs ?? REPO_SCAN_TTL_MS
|
|
8778
|
+
});
|
|
8779
|
+
let scannedAt = cache.scannedAt;
|
|
8780
|
+
if (decision.scan) {
|
|
8572
8781
|
const startedAt = Date.now();
|
|
8573
8782
|
const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
|
|
8574
8783
|
let added = 0;
|
|
@@ -8577,9 +8786,10 @@ async function refreshRepoMap(opts) {
|
|
|
8577
8786
|
take(full, paths[0], "scan", paths.slice(1));
|
|
8578
8787
|
added++;
|
|
8579
8788
|
}
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8789
|
+
scannedAt = stamp();
|
|
8790
|
+
log(
|
|
8791
|
+
`[overmind] \u043F\u0440\u043E\u0448\u0451\u043B \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C (${SCAN_REASON_TEXT[decision.reason]}): ${added > 0 ? `\u043D\u0430\u0448\u0451\u043B \u0435\u0449\u0451 ${added}` : "\u043D\u043E\u0432\u043E\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u0451\u043B"}, \u0432\u0441\u0435\u0433\u043E \u043A\u043B\u043E\u043D\u043E\u0432 ${clones.size}, \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441. \u0418\u0441\u043A\u0430\u043B \u0432: ${opts.roots.length ? opts.roots.slice(0, 6).join(", ") : "\u043D\u0438\u0433\u0434\u0435 \u2014 \u043F\u043E\u0438\u0441\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)"}`
|
|
8792
|
+
);
|
|
8583
8793
|
}
|
|
8584
8794
|
const registry = [...clones.values()].map((c) => ({
|
|
8585
8795
|
repoFullName: c.repoFullName,
|
|
@@ -8587,10 +8797,12 @@ async function refreshRepoMap(opts) {
|
|
|
8587
8797
|
}));
|
|
8588
8798
|
writeRepoMapCache(
|
|
8589
8799
|
opts.cachePath,
|
|
8590
|
-
[...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
|
|
8800
|
+
[...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path })),
|
|
8801
|
+
scannedAt
|
|
8591
8802
|
);
|
|
8592
8803
|
if (!answer.ok) {
|
|
8593
8804
|
log(describeReposFailure(answer.kind, answer.detail));
|
|
8805
|
+
log(`[overmind] \u0438\u0442\u043E\u0433: \u043A\u043B\u043E\u043D\u043E\u0432 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E ${clones.size}, \u0441\u043F\u0438\u0441\u043E\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0441\u043C. \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435)`);
|
|
8594
8806
|
return registry;
|
|
8595
8807
|
}
|
|
8596
8808
|
opts.state.unknown = false;
|
|
@@ -8602,7 +8814,7 @@ async function refreshRepoMap(opts) {
|
|
|
8602
8814
|
log("[overmind] \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435");
|
|
8603
8815
|
return registry;
|
|
8604
8816
|
}
|
|
8605
|
-
log(`[overmind]
|
|
8817
|
+
log(`[overmind] ${describeMapSummary(opts.state, opts.roots)}`);
|
|
8606
8818
|
for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
|
|
8607
8819
|
const prefixOf = new Map(answer.repos.map((r) => [r.repoFullName, r.taskPrefix]));
|
|
8608
8820
|
for (const name of opts.state.missing) {
|
|
@@ -8611,6 +8823,11 @@ async function refreshRepoMap(opts) {
|
|
|
8611
8823
|
}
|
|
8612
8824
|
return registry;
|
|
8613
8825
|
}
|
|
8826
|
+
function describeMapSummary(state, roots) {
|
|
8827
|
+
const found = state.clones.length;
|
|
8828
|
+
const where = roots.length ? roots.slice(0, 6).join(", ") : "\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)";
|
|
8829
|
+
return `\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${found + state.missing.length}): \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 ${found}, \u0431\u0435\u0437 \u043A\u043B\u043E\u043D\u0430 ${state.missing.length}. \u0418\u0441\u043A\u0430\u043B \u0432: ${where}`;
|
|
8830
|
+
}
|
|
8614
8831
|
function describeClone(c) {
|
|
8615
8832
|
const how = c.source === "configured" ? "\u043F\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C" : c.source === "cache" ? "\u0438\u0437 \u043A\u044D\u0448\u0430 \u043C\u0430\u0448\u0438\u043D\u044B" : "\u043D\u0430\u0439\u0434\u0435\u043D \u0441\u043A\u0430\u043D\u043E\u043C";
|
|
8616
8833
|
const alt = c.alternatives.length ? ` \u26A0 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u0435\u0449\u0451 ${c.alternatives.length}: ${c.alternatives.join(", ")} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u0432 \u043F\u0435\u0440\u0432\u043E\u043C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443 \u043A\u043E\u0440\u043D\u0435\u0439` : "";
|
|
@@ -8631,40 +8848,43 @@ function repoMapRows(state) {
|
|
|
8631
8848
|
...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
|
|
8632
8849
|
];
|
|
8633
8850
|
}
|
|
8634
|
-
function
|
|
8851
|
+
function loadRepoMapCache(path) {
|
|
8635
8852
|
try {
|
|
8636
8853
|
const raw = JSON.parse(readFileSync9(path, "utf8"));
|
|
8637
|
-
if (!Array.isArray(raw?.entries)) return [];
|
|
8638
|
-
return
|
|
8639
|
-
|
|
8640
|
-
|
|
8854
|
+
if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
|
|
8855
|
+
return {
|
|
8856
|
+
scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
|
|
8857
|
+
entries: raw.entries.filter(
|
|
8858
|
+
(e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
|
|
8859
|
+
)
|
|
8860
|
+
};
|
|
8641
8861
|
} catch {
|
|
8642
|
-
return [];
|
|
8862
|
+
return { scannedAt: null, entries: [] };
|
|
8643
8863
|
}
|
|
8644
8864
|
}
|
|
8645
|
-
function writeRepoMapCache(path, entries) {
|
|
8865
|
+
function writeRepoMapCache(path, entries, scannedAt = stamp()) {
|
|
8646
8866
|
try {
|
|
8647
8867
|
const tmp = `${path}.tmp`;
|
|
8648
|
-
writeFileSync8(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
|
|
8649
|
-
|
|
8868
|
+
writeFileSync8(tmp, JSON.stringify({ at: stamp(), scannedAt, entries }, null, 2));
|
|
8869
|
+
renameSync6(tmp, path);
|
|
8650
8870
|
} catch {
|
|
8651
8871
|
}
|
|
8652
8872
|
}
|
|
8653
8873
|
|
|
8654
8874
|
// src/hostIdentity.ts
|
|
8655
8875
|
import { hostname } from "os";
|
|
8656
|
-
import { existsSync as
|
|
8657
|
-
import { join as
|
|
8876
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
8877
|
+
import { join as join12 } from "path";
|
|
8658
8878
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8659
8879
|
function hostIdentityPath(home) {
|
|
8660
|
-
return
|
|
8880
|
+
return join12(home, "host.json");
|
|
8661
8881
|
}
|
|
8662
8882
|
function loadHostIdentity(deps) {
|
|
8663
|
-
const exists = deps.exists ??
|
|
8883
|
+
const exists = deps.exists ?? existsSync8;
|
|
8664
8884
|
const readFile2 = deps.readFile ?? ((p) => readFileSync10(p, "utf8"));
|
|
8665
8885
|
const writeFile3 = deps.writeFile ?? ((p, data) => {
|
|
8666
8886
|
writeFileSync9(`${p}.tmp`, data);
|
|
8667
|
-
|
|
8887
|
+
renameSync7(`${p}.tmp`, p);
|
|
8668
8888
|
});
|
|
8669
8889
|
const host = deps.hostname ?? hostname;
|
|
8670
8890
|
const newId = deps.newId ?? randomUUID2;
|
|
@@ -8846,15 +9066,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
|
8846
9066
|
|
|
8847
9067
|
// src/launcher.ts
|
|
8848
9068
|
import { spawn } from "child_process";
|
|
8849
|
-
import { closeSync as closeSync2, existsSync as
|
|
9069
|
+
import { closeSync as closeSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, openSync as openSync2, readdirSync as readdirSync8 } from "fs";
|
|
8850
9070
|
import { createRequire } from "module";
|
|
8851
9071
|
import { homedir as homedir4 } from "os";
|
|
8852
|
-
import { dirname as dirname2, join as
|
|
9072
|
+
import { dirname as dirname2, join as join14 } from "path";
|
|
8853
9073
|
import { fileURLToPath } from "url";
|
|
8854
9074
|
|
|
8855
9075
|
// src/droneHome.ts
|
|
8856
|
-
import { lstatSync, mkdirSync as
|
|
8857
|
-
import { join as
|
|
9076
|
+
import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync7, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
|
|
9077
|
+
import { join as join13, relative, sep } from "path";
|
|
8858
9078
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
8859
9079
|
function droneHomeDeny(env = process.env) {
|
|
8860
9080
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -8872,29 +9092,29 @@ function buildDroneHome(opts) {
|
|
|
8872
9092
|
const { realHome, droneHome } = opts;
|
|
8873
9093
|
const deny = opts.deny ?? droneHomeDeny();
|
|
8874
9094
|
const platform = opts.platform ?? process.platform;
|
|
8875
|
-
|
|
8876
|
-
const wanted = new Set(mirroredEntries(
|
|
9095
|
+
mkdirSync6(droneHome, { recursive: true, mode: 448 });
|
|
9096
|
+
const wanted = new Set(mirroredEntries(readdirSync7(realHome), deny, platform));
|
|
8877
9097
|
const rel = relative(realHome, droneHome);
|
|
8878
9098
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
8879
|
-
for (const existing of
|
|
9099
|
+
for (const existing of readdirSync7(droneHome)) {
|
|
8880
9100
|
if (wanted.has(existing)) continue;
|
|
8881
|
-
const p =
|
|
9101
|
+
const p = join13(droneHome, existing);
|
|
8882
9102
|
try {
|
|
8883
9103
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
8884
9104
|
} catch {
|
|
8885
9105
|
}
|
|
8886
9106
|
}
|
|
8887
9107
|
for (const name of wanted) {
|
|
8888
|
-
const link =
|
|
9108
|
+
const link = join13(droneHome, name);
|
|
8889
9109
|
try {
|
|
8890
9110
|
lstatSync(link);
|
|
8891
9111
|
continue;
|
|
8892
9112
|
} catch {
|
|
8893
9113
|
}
|
|
8894
|
-
const target =
|
|
9114
|
+
const target = join13(realHome, name);
|
|
8895
9115
|
let isDir;
|
|
8896
9116
|
try {
|
|
8897
|
-
isDir =
|
|
9117
|
+
isDir = statSync7(target).isDirectory();
|
|
8898
9118
|
} catch {
|
|
8899
9119
|
continue;
|
|
8900
9120
|
}
|
|
@@ -8912,7 +9132,7 @@ var REAL_PATH_ENV_PINS = [
|
|
|
8912
9132
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
8913
9133
|
];
|
|
8914
9134
|
function realPathEnvPins(realHome) {
|
|
8915
|
-
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env,
|
|
9135
|
+
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join13(realHome, ...segments)]));
|
|
8916
9136
|
}
|
|
8917
9137
|
function dronePrivatePaths(realHome, swarmHome) {
|
|
8918
9138
|
return [
|
|
@@ -9024,7 +9244,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
|
9024
9244
|
const env = { ...base3 };
|
|
9025
9245
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
9026
9246
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
9027
|
-
env.TURBO_CACHE_DIR =
|
|
9247
|
+
env.TURBO_CACHE_DIR = join14(worktree, ".turbo", "cache");
|
|
9028
9248
|
}
|
|
9029
9249
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
9030
9250
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -9052,20 +9272,21 @@ var DAEMON_STATE_ENV = [
|
|
|
9052
9272
|
"TASKLESS_TOKEN"
|
|
9053
9273
|
];
|
|
9054
9274
|
function bundledRoleDir(kind) {
|
|
9055
|
-
return
|
|
9275
|
+
return join14(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
|
|
9056
9276
|
}
|
|
9057
9277
|
function roleDir(worktree, kind) {
|
|
9058
|
-
return
|
|
9278
|
+
return join14(worktree, ".claude", "roles", kind);
|
|
9059
9279
|
}
|
|
9060
9280
|
function roleDirs(worktree, kind) {
|
|
9061
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
9281
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync9(dir));
|
|
9062
9282
|
}
|
|
9063
|
-
function nextRunLogPath(runLogsDir,
|
|
9064
|
-
const dir =
|
|
9065
|
-
|
|
9283
|
+
function nextRunLogPath(runLogsDir, runLogKeys) {
|
|
9284
|
+
const dir = runLogDirForWrite(runLogsDir, runLogKeys);
|
|
9285
|
+
if (!dir) throw new Error("\u043D\u0435\u0447\u0435\u043C \u0430\u0434\u0440\u0435\u0441\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 run-log'\u043E\u0432: \u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435\u0442 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u044E\u0447\u0430 (SwarmStore.runLogKeys)");
|
|
9286
|
+
mkdirSync7(dir, { recursive: true });
|
|
9066
9287
|
let files = [];
|
|
9067
9288
|
try {
|
|
9068
|
-
files =
|
|
9289
|
+
files = readdirSync8(dir);
|
|
9069
9290
|
} catch {
|
|
9070
9291
|
}
|
|
9071
9292
|
let next = 0;
|
|
@@ -9073,11 +9294,11 @@ function nextRunLogPath(runLogsDir, runId) {
|
|
|
9073
9294
|
const m = /^(\d+)\.json/.exec(f);
|
|
9074
9295
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
9075
9296
|
}
|
|
9076
|
-
return
|
|
9297
|
+
return join14(dir, `${next}.json`);
|
|
9077
9298
|
}
|
|
9078
|
-
function openNextRunLog(runLogsDir,
|
|
9299
|
+
function openNextRunLog(runLogsDir, runLogKeys) {
|
|
9079
9300
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
9080
|
-
const path = nextRunLogPath(runLogsDir,
|
|
9301
|
+
const path = nextRunLogPath(runLogsDir, runLogKeys);
|
|
9081
9302
|
let out;
|
|
9082
9303
|
try {
|
|
9083
9304
|
out = openSync2(path, "wx");
|
|
@@ -9093,7 +9314,7 @@ function openNextRunLog(runLogsDir, runId) {
|
|
|
9093
9314
|
}
|
|
9094
9315
|
return { out, err };
|
|
9095
9316
|
}
|
|
9096
|
-
throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \
|
|
9317
|
+
throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \u043F\u0440\u043E\u0433\u043E\u043D\u0430 (${runLogKeys[0]}) \u2014 \u0433\u043E\u043D\u043A\u0430?`);
|
|
9097
9318
|
}
|
|
9098
9319
|
function makeLauncher(cfg) {
|
|
9099
9320
|
return ({ worktree, run, prompt, continueSession }) => {
|
|
@@ -9156,9 +9377,9 @@ function makeLauncher(cfg) {
|
|
|
9156
9377
|
}
|
|
9157
9378
|
const realHome = cfg.droneIsolation?.realHome ?? homedir4();
|
|
9158
9379
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
9159
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !
|
|
9380
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync9(join14(cfg.droneIsolation.swarmHome, "token"))) {
|
|
9160
9381
|
console.error(
|
|
9161
|
-
`[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 ${
|
|
9382
|
+
`[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 ${join14(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.`
|
|
9162
9383
|
);
|
|
9163
9384
|
}
|
|
9164
9385
|
const executor = cfg.executor ?? resolveExecutor();
|
|
@@ -9185,7 +9406,13 @@ function makeLauncher(cfg) {
|
|
|
9185
9406
|
let outFd;
|
|
9186
9407
|
let errFd;
|
|
9187
9408
|
if (cfg.runLogsDir) {
|
|
9188
|
-
const
|
|
9409
|
+
const logKeys = cfg.runLogKeys?.(run) ?? [];
|
|
9410
|
+
if (logKeys.length === 0) {
|
|
9411
|
+
throw new Error(
|
|
9412
|
+
`\u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id} (${run.task_slug}) \u043D\u0435\u0447\u0435\u043C \u0430\u0434\u0440\u0435\u0441\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 run-log'\u043E\u0432: \u0443 \u0441\u0442\u043E\u0440\u0430 \u043D\u0435\u0442 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u044E\u0447\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430. \u0414\u0440\u043E\u043D \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0435\u043D \u2014 \u0438\u043D\u0430\u0447\u0435 \u0435\u0433\u043E \u043B\u043E\u0433 \u043B\u0451\u0433 \u0431\u044B \u0432 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043A\u0430\u043A \u0447\u0443\u0436\u043E\u0439 (TASWM-419).`
|
|
9413
|
+
);
|
|
9414
|
+
}
|
|
9415
|
+
const fds = openNextRunLog(cfg.runLogsDir, logKeys);
|
|
9189
9416
|
outFd = fds.out;
|
|
9190
9417
|
errFd = fds.err ?? void 0;
|
|
9191
9418
|
stdio = ["ignore", outFd, errFd ?? "ignore"];
|
|
@@ -9213,21 +9440,21 @@ function makeLauncher(cfg) {
|
|
|
9213
9440
|
init_procCleanup();
|
|
9214
9441
|
|
|
9215
9442
|
// src/cost.ts
|
|
9443
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync9, renameSync as renameSync8 } from "fs";
|
|
9444
|
+
import { join as join15 } from "path";
|
|
9216
9445
|
init_executors();
|
|
9217
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
|
|
9218
|
-
import { join as join14 } from "path";
|
|
9219
9446
|
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
9220
9447
|
function unprocessedLogFiles(dir) {
|
|
9221
9448
|
try {
|
|
9222
|
-
return
|
|
9449
|
+
return readdirSync9(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
9223
9450
|
} catch {
|
|
9224
9451
|
return [];
|
|
9225
9452
|
}
|
|
9226
9453
|
}
|
|
9227
9454
|
function accountRunCost(deps, run) {
|
|
9228
|
-
const dir =
|
|
9229
|
-
const files = unprocessedLogFiles(dir);
|
|
9230
|
-
if (files.length === 0) return;
|
|
9455
|
+
const dir = findRunLogDir(deps.runLogsDir, deps.db.runLogKeys(run.id));
|
|
9456
|
+
const files = dir ? unprocessedLogFiles(dir) : [];
|
|
9457
|
+
if (!dir || files.length === 0) return;
|
|
9231
9458
|
const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
|
|
9232
9459
|
const executor = deps.executor ?? resolveExecutor();
|
|
9233
9460
|
let added = 0;
|
|
@@ -9236,7 +9463,7 @@ function accountRunCost(deps, run) {
|
|
|
9236
9463
|
let durationMs = 0;
|
|
9237
9464
|
const processed = [];
|
|
9238
9465
|
for (const f of files) {
|
|
9239
|
-
const full =
|
|
9466
|
+
const full = join15(dir, f);
|
|
9240
9467
|
let raw = null;
|
|
9241
9468
|
try {
|
|
9242
9469
|
raw = readFileSync11(full, "utf8");
|
|
@@ -9265,7 +9492,7 @@ function accountRunCost(deps, run) {
|
|
|
9265
9492
|
}
|
|
9266
9493
|
for (const full of processed) {
|
|
9267
9494
|
try {
|
|
9268
|
-
|
|
9495
|
+
renameSync8(full, `${full}.done`);
|
|
9269
9496
|
} catch {
|
|
9270
9497
|
}
|
|
9271
9498
|
}
|
|
@@ -9273,20 +9500,34 @@ function accountRunCost(deps, run) {
|
|
|
9273
9500
|
function makeCostAccountant(deps) {
|
|
9274
9501
|
return (run) => accountRunCost(deps, run);
|
|
9275
9502
|
}
|
|
9503
|
+
function sweepPendingRunCosts(deps) {
|
|
9504
|
+
for (const run of deps.db.listRuns()) {
|
|
9505
|
+
if (run.pid != null && deps.isAlive(run.pid)) continue;
|
|
9506
|
+
try {
|
|
9507
|
+
accountRunCost(deps, run);
|
|
9508
|
+
} catch (e) {
|
|
9509
|
+
console.error(`[cost] \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}: \u0443\u0447\u0451\u0442 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F \u2014 ${String(e)}`);
|
|
9510
|
+
}
|
|
9511
|
+
}
|
|
9512
|
+
}
|
|
9513
|
+
function makeCostSweeper(deps) {
|
|
9514
|
+
return () => sweepPendingRunCosts(deps);
|
|
9515
|
+
}
|
|
9276
9516
|
|
|
9277
9517
|
// src/runOutcome.ts
|
|
9518
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "fs";
|
|
9519
|
+
import { join as join16 } from "path";
|
|
9278
9520
|
init_executors();
|
|
9279
|
-
import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
|
|
9280
|
-
import { join as join15 } from "path";
|
|
9281
9521
|
function endedItself(outcome) {
|
|
9282
9522
|
return outcome === "graceful" || outcome === "budget";
|
|
9283
9523
|
}
|
|
9284
9524
|
var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
|
|
9285
|
-
function classifyExit(runLogsDir,
|
|
9286
|
-
const dir =
|
|
9525
|
+
function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
|
|
9526
|
+
const dir = findRunLogDir(runLogsDir, runLogKeys);
|
|
9527
|
+
if (!dir) return "unknown";
|
|
9287
9528
|
let files;
|
|
9288
9529
|
try {
|
|
9289
|
-
files =
|
|
9530
|
+
files = readdirSync10(dir);
|
|
9290
9531
|
} catch {
|
|
9291
9532
|
return "unknown";
|
|
9292
9533
|
}
|
|
@@ -9300,7 +9541,7 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
|
|
|
9300
9541
|
if (!latest) return "unknown";
|
|
9301
9542
|
let raw;
|
|
9302
9543
|
try {
|
|
9303
|
-
raw = readFileSync12(
|
|
9544
|
+
raw = readFileSync12(join16(dir, latest.name), "utf8");
|
|
9304
9545
|
} catch {
|
|
9305
9546
|
return "unknown";
|
|
9306
9547
|
}
|
|
@@ -9308,9 +9549,9 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
|
|
|
9308
9549
|
if (!result) return "crash";
|
|
9309
9550
|
return result.stoppedByBudget ? "budget" : "graceful";
|
|
9310
9551
|
}
|
|
9311
|
-
function makeExitClassifier(runLogsDir, executor) {
|
|
9552
|
+
function makeExitClassifier(runLogsDir, runLogKeys, executor) {
|
|
9312
9553
|
const exec = executor ?? resolveExecutor();
|
|
9313
|
-
return (run) => classifyExit(runLogsDir, run.id, exec);
|
|
9554
|
+
return (run) => classifyExit(runLogsDir, runLogKeys(run.id), exec);
|
|
9314
9555
|
}
|
|
9315
9556
|
|
|
9316
9557
|
// src/orchestrator.ts
|
|
@@ -9318,20 +9559,20 @@ init_src();
|
|
|
9318
9559
|
init_git();
|
|
9319
9560
|
|
|
9320
9561
|
// src/deps.ts
|
|
9321
|
-
import { existsSync as
|
|
9322
|
-
import { join as
|
|
9562
|
+
import { existsSync as existsSync11, readdirSync as readdirSync12, readFileSync as readFileSync13 } from "fs";
|
|
9563
|
+
import { join as join19 } from "path";
|
|
9323
9564
|
import { execa } from "execa";
|
|
9324
9565
|
|
|
9325
9566
|
// src/prepareLock.ts
|
|
9326
9567
|
import { createHash } from "crypto";
|
|
9327
|
-
import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as
|
|
9328
|
-
import { join as
|
|
9568
|
+
import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync9, writeSync } from "fs";
|
|
9569
|
+
import { join as join18 } from "path";
|
|
9329
9570
|
import { tmpdir } from "os";
|
|
9330
9571
|
var WAIT_TIMEOUT_MS = 5 * 6e4;
|
|
9331
9572
|
var POLL_MS = 300;
|
|
9332
9573
|
var STALE_MS = 20 * 6e4;
|
|
9333
9574
|
function machineLockPath(key, dir = tmpdir()) {
|
|
9334
|
-
return
|
|
9575
|
+
return join18(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
|
|
9335
9576
|
}
|
|
9336
9577
|
function defaultTryCreate(path, content) {
|
|
9337
9578
|
try {
|
|
@@ -9348,7 +9589,7 @@ function defaultTryCreate(path, content) {
|
|
|
9348
9589
|
}
|
|
9349
9590
|
function defaultAgeMs(path) {
|
|
9350
9591
|
try {
|
|
9351
|
-
return Date.now() -
|
|
9592
|
+
return Date.now() - statSync9(path).mtimeMs;
|
|
9352
9593
|
} catch {
|
|
9353
9594
|
return null;
|
|
9354
9595
|
}
|
|
@@ -9412,14 +9653,14 @@ function safeReadText(p) {
|
|
|
9412
9653
|
}
|
|
9413
9654
|
function safeListDirs(p) {
|
|
9414
9655
|
try {
|
|
9415
|
-
return
|
|
9656
|
+
return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9416
9657
|
} catch {
|
|
9417
9658
|
return [];
|
|
9418
9659
|
}
|
|
9419
9660
|
}
|
|
9420
9661
|
function safeListFiles(p) {
|
|
9421
9662
|
try {
|
|
9422
|
-
return
|
|
9663
|
+
return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
9423
9664
|
} catch {
|
|
9424
9665
|
return [];
|
|
9425
9666
|
}
|
|
@@ -9445,21 +9686,21 @@ function overridePlan(override) {
|
|
|
9445
9686
|
};
|
|
9446
9687
|
}
|
|
9447
9688
|
function detectPackageManager(worktree, io = {}) {
|
|
9448
|
-
const exists = io.exists ??
|
|
9689
|
+
const exists = io.exists ?? existsSync11;
|
|
9449
9690
|
const readText = io.readText ?? safeReadText;
|
|
9450
|
-
const hasPkgJson = exists(
|
|
9691
|
+
const hasPkgJson = exists(join19(worktree, "package.json"));
|
|
9451
9692
|
if (!hasPkgJson) {
|
|
9452
|
-
if (exists(
|
|
9693
|
+
if (exists(join19(worktree, "deno.json")) || exists(join19(worktree, "deno.jsonc")) || exists(join19(worktree, "deno.lock"))) return "deno";
|
|
9453
9694
|
return null;
|
|
9454
9695
|
}
|
|
9455
|
-
const pkg = readJson(readText,
|
|
9696
|
+
const pkg = readJson(readText, join19(worktree, "package.json"));
|
|
9456
9697
|
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
|
|
9457
9698
|
if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
|
|
9458
|
-
if (exists(
|
|
9459
|
-
if (exists(
|
|
9460
|
-
if (exists(
|
|
9461
|
-
if (exists(
|
|
9462
|
-
if (exists(
|
|
9699
|
+
if (exists(join19(worktree, "bun.lock")) || exists(join19(worktree, "bun.lockb"))) return "bun";
|
|
9700
|
+
if (exists(join19(worktree, "pnpm-lock.yaml"))) return "pnpm";
|
|
9701
|
+
if (exists(join19(worktree, "yarn.lock"))) return "yarn";
|
|
9702
|
+
if (exists(join19(worktree, "package-lock.json"))) return "npm";
|
|
9703
|
+
if (exists(join19(worktree, "deno.lock"))) return "deno";
|
|
9463
9704
|
return "npm";
|
|
9464
9705
|
}
|
|
9465
9706
|
function jsInstallCommand(pm) {
|
|
@@ -9512,7 +9753,7 @@ var ECOSYSTEM_MARKERS = [
|
|
|
9512
9753
|
// конфигурация — дело сборки, не подготовки
|
|
9513
9754
|
];
|
|
9514
9755
|
function detectPlan(worktree, io) {
|
|
9515
|
-
const exists = io.exists ??
|
|
9756
|
+
const exists = io.exists ?? existsSync11;
|
|
9516
9757
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
9517
9758
|
const pm = detectPackageManager(worktree, io);
|
|
9518
9759
|
if (pm) {
|
|
@@ -9520,7 +9761,7 @@ function detectPlan(worktree, io) {
|
|
|
9520
9761
|
// node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
|
|
9521
9762
|
// У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
|
|
9522
9763
|
// заново, но он дешёвый: тянет из того же общего кэша.
|
|
9523
|
-
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists:
|
|
9764
|
+
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join19(worktree, "node_modules"), source: "detect" }
|
|
9524
9765
|
];
|
|
9525
9766
|
for (const target of workspaceBuildTargets(worktree, io)) {
|
|
9526
9767
|
commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
|
|
@@ -9529,12 +9770,12 @@ function detectPlan(worktree, io) {
|
|
|
9529
9770
|
return { source: "detect", ecosystem: `node/${pm}`, commands };
|
|
9530
9771
|
}
|
|
9531
9772
|
for (const marker of ECOSYSTEM_MARKERS) {
|
|
9532
|
-
const hit = marker.files.some((f) => exists(
|
|
9773
|
+
const hit = marker.files.some((f) => exists(join19(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
|
|
9533
9774
|
if (!hit) continue;
|
|
9534
9775
|
let ecosystem = marker.ecosystem;
|
|
9535
9776
|
let command = marker.command;
|
|
9536
9777
|
for (const refine of marker.refine ?? []) {
|
|
9537
|
-
if (!exists(
|
|
9778
|
+
if (!exists(join19(worktree, refine.file))) continue;
|
|
9538
9779
|
ecosystem = refine.ecosystem;
|
|
9539
9780
|
command = refine.command;
|
|
9540
9781
|
break;
|
|
@@ -9591,12 +9832,12 @@ var CI_INSTALL_PATTERNS = [
|
|
|
9591
9832
|
function ciWorkflowPlan(worktree, io) {
|
|
9592
9833
|
const readText = io.readText ?? safeReadText;
|
|
9593
9834
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
9594
|
-
const dir =
|
|
9835
|
+
const dir = join19(worktree, ".github", "workflows");
|
|
9595
9836
|
const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
|
|
9596
9837
|
const seen = /* @__PURE__ */ new Set();
|
|
9597
9838
|
const commands = [];
|
|
9598
9839
|
for (const name of files.sort()) {
|
|
9599
|
-
const yaml = readText(
|
|
9840
|
+
const yaml = readText(join19(dir, name));
|
|
9600
9841
|
if (yaml === null) continue;
|
|
9601
9842
|
for (const raw of workflowRunLines(yaml)) {
|
|
9602
9843
|
const line = raw.replace(/\s+/g, " ").trim();
|
|
@@ -9611,7 +9852,7 @@ function ciWorkflowPlan(worktree, io) {
|
|
|
9611
9852
|
return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
|
|
9612
9853
|
}
|
|
9613
9854
|
function workspaceGlobs(worktree, readText) {
|
|
9614
|
-
const yaml = readText(
|
|
9855
|
+
const yaml = readText(join19(worktree, "pnpm-workspace.yaml"));
|
|
9615
9856
|
if (yaml !== null) {
|
|
9616
9857
|
const globs = [];
|
|
9617
9858
|
let inPackages = false;
|
|
@@ -9631,7 +9872,7 @@ function workspaceGlobs(worktree, readText) {
|
|
|
9631
9872
|
}
|
|
9632
9873
|
if (globs.length) return globs;
|
|
9633
9874
|
}
|
|
9634
|
-
const pkg = readJson(readText,
|
|
9875
|
+
const pkg = readJson(readText, join19(worktree, "package.json"));
|
|
9635
9876
|
const ws = pkg?.workspaces;
|
|
9636
9877
|
const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
9637
9878
|
return list.filter((g) => typeof g === "string");
|
|
@@ -9642,14 +9883,14 @@ function workspaceDirs(worktree, globs, io) {
|
|
|
9642
9883
|
if (glob.startsWith("!")) continue;
|
|
9643
9884
|
const star = glob.indexOf("*");
|
|
9644
9885
|
if (star === -1) {
|
|
9645
|
-
const dir =
|
|
9646
|
-
if (io.exists(
|
|
9886
|
+
const dir = join19(worktree, glob);
|
|
9887
|
+
if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
|
|
9647
9888
|
continue;
|
|
9648
9889
|
}
|
|
9649
|
-
const base3 =
|
|
9890
|
+
const base3 = join19(worktree, glob.slice(0, star).replace(/\/$/, ""));
|
|
9650
9891
|
for (const name of io.listDirs(base3)) {
|
|
9651
|
-
const dir =
|
|
9652
|
-
if (io.exists(
|
|
9892
|
+
const dir = join19(base3, name);
|
|
9893
|
+
if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
|
|
9653
9894
|
}
|
|
9654
9895
|
}
|
|
9655
9896
|
return [...dirs];
|
|
@@ -9670,18 +9911,18 @@ function entryPaths(pkg) {
|
|
|
9670
9911
|
return out;
|
|
9671
9912
|
}
|
|
9672
9913
|
function workspaceBuildTargets(worktree, io = {}) {
|
|
9673
|
-
const exists = io.exists ??
|
|
9914
|
+
const exists = io.exists ?? existsSync11;
|
|
9674
9915
|
const readText = io.readText ?? safeReadText;
|
|
9675
9916
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
9676
9917
|
const globs = workspaceGlobs(worktree, readText);
|
|
9677
9918
|
if (!globs.length) return [];
|
|
9678
9919
|
const pkgs = [];
|
|
9679
9920
|
for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
|
|
9680
|
-
const pkg = readJson(readText,
|
|
9921
|
+
const pkg = readJson(readText, join19(dir, "package.json"));
|
|
9681
9922
|
if (!pkg || typeof pkg.name !== "string") continue;
|
|
9682
9923
|
const scripts = pkg.scripts ?? {};
|
|
9683
9924
|
const entries = entryPaths(pkg);
|
|
9684
|
-
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(
|
|
9925
|
+
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join19(dir, e)));
|
|
9685
9926
|
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
9686
9927
|
pkgs.push({ name: pkg.name, dir, deps, needsBuild });
|
|
9687
9928
|
}
|
|
@@ -9709,7 +9950,7 @@ function devDatabaseUrl(worktree, io = {}) {
|
|
|
9709
9950
|
const fromEnv = (io.env ?? process.env).DATABASE_URL;
|
|
9710
9951
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
9711
9952
|
for (const file of [".env", ".env.example"]) {
|
|
9712
|
-
const raw = readText(
|
|
9953
|
+
const raw = readText(join19(worktree, file));
|
|
9713
9954
|
if (raw === null) continue;
|
|
9714
9955
|
const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
|
|
9715
9956
|
const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
|
|
@@ -9725,11 +9966,11 @@ function isLocalDatabase(url) {
|
|
|
9725
9966
|
}
|
|
9726
9967
|
}
|
|
9727
9968
|
function prismaPackages(worktree, io) {
|
|
9728
|
-
const exists = io.exists ??
|
|
9969
|
+
const exists = io.exists ?? existsSync11;
|
|
9729
9970
|
const readText = io.readText ?? safeReadText;
|
|
9730
9971
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
9731
9972
|
const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
|
|
9732
|
-
return dirs.filter((dir) => exists(
|
|
9973
|
+
return dirs.filter((dir) => exists(join19(dir, "prisma", "schema.prisma")) && listDirs(join19(dir, "prisma", "migrations")).length > 0);
|
|
9733
9974
|
}
|
|
9734
9975
|
function jsExecBin(pm, command) {
|
|
9735
9976
|
switch (pm) {
|
|
@@ -9762,7 +10003,7 @@ function planPrepare(worktree, io = {}) {
|
|
|
9762
10003
|
return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
|
|
9763
10004
|
}
|
|
9764
10005
|
async function ensureWorktreeDeps(worktree, io = {}) {
|
|
9765
|
-
const exists = io.exists ??
|
|
10006
|
+
const exists = io.exists ?? existsSync11;
|
|
9766
10007
|
const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
|
|
9767
10008
|
const approve = io.approve ?? (() => true);
|
|
9768
10009
|
const plan = planPrepare(worktree, io);
|
|
@@ -9947,6 +10188,43 @@ function pruneDefers(episodes, live) {
|
|
|
9947
10188
|
for (const key of episodes.keys()) if (!live.has(key)) episodes.delete(key);
|
|
9948
10189
|
}
|
|
9949
10190
|
|
|
10191
|
+
// src/spawnAttempts.ts
|
|
10192
|
+
init_src();
|
|
10193
|
+
var PRE_START_ATTEMPT_LIMIT = 3;
|
|
10194
|
+
var PRE_START_MARK = "\u0434\u0440\u043E\u043D \u043D\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u0430\u043B";
|
|
10195
|
+
var SPAWN_PARK_MARK = "\u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u0430 \u0440\u043E\u0435\u043C";
|
|
10196
|
+
function isPreStartFailure(run) {
|
|
10197
|
+
return run.status === "failed" && (run.stop_reason ?? "").includes(PRE_START_MARK);
|
|
10198
|
+
}
|
|
10199
|
+
function isSpawnParked(run) {
|
|
10200
|
+
return (run.stop_reason ?? "").includes(SPAWN_PARK_MARK);
|
|
10201
|
+
}
|
|
10202
|
+
var SPAWN_ATTEMPT_WINDOW_MS = 60 * 6e4;
|
|
10203
|
+
function preStartAttempts(runs, taskSlug, opts = {}) {
|
|
10204
|
+
const windowMs = opts.windowMs ?? SPAWN_ATTEMPT_WINDOW_MS;
|
|
10205
|
+
const now = opts.now ?? Date.now();
|
|
10206
|
+
const own = runs.filter((r) => r.task_slug === taskSlug && r.reviews_run_id == null && r.resolves_run_id == null && r.verifies_run_id == null).sort((a, b) => b.id - a.id);
|
|
10207
|
+
let n = 0;
|
|
10208
|
+
for (const run of own) {
|
|
10209
|
+
if (run.status === "queued" || run.status === "running") continue;
|
|
10210
|
+
if (isSpawnParked(run) && run.status !== "failed") continue;
|
|
10211
|
+
if (!isPreStartFailure(run)) break;
|
|
10212
|
+
const at = run.created_at ? stampMs(run.created_at) : Number.NaN;
|
|
10213
|
+
if (Number.isFinite(at) && now - at >= windowMs) break;
|
|
10214
|
+
n += 1;
|
|
10215
|
+
}
|
|
10216
|
+
return n;
|
|
10217
|
+
}
|
|
10218
|
+
function unresolvedRepoReason(repo, served) {
|
|
10219
|
+
const raw = repo.trim();
|
|
10220
|
+
if (!raw || raw.startsWith("/") || raw.startsWith("~") || /^[a-z]:[\\/]/i.test(raw)) return null;
|
|
10221
|
+
const list = served.length ? served.join(", ") : "\u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E (\u0440\u0435\u0435\u0441\u0442\u0440 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u043F\u0443\u0441\u0442)";
|
|
10222
|
+
return `\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 ${raw} \u043D\u0435 \u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0443\u0442\u044C: \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0435\u0433\u043E \u043A\u043B\u043E\u043D\u0430 \u043D\u0435\u0442. \u041C\u0430\u0448\u0438\u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u0435\u0442: ${list}. \u041A\u043B\u043E\u043D\u0438\u0440\u0443\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0438 \u0434\u043E\u0431\u0430\u0432\u044C \u0435\u0433\u043E \u0432 SWARM_REPO_PATHS (\u0438\u043B\u0438 swarm start --repo <\u043F\u0443\u0442\u044C>), \u043B\u0438\u0431\u043E \u043E\u0442\u0434\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u043C\u0430\u0448\u0438\u043D\u0435, \u0433\u0434\u0435 \u043E\u043D \u0435\u0441\u0442\u044C.`;
|
|
10223
|
+
}
|
|
10224
|
+
function parkReason(attempts, reason) {
|
|
10225
|
+
return `${PRE_START_MARK} ${attempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u043E \u043A\u0440\u0443\u0433\u0443. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
|
|
10226
|
+
}
|
|
10227
|
+
|
|
9950
10228
|
// src/overmind.ts
|
|
9951
10229
|
init_git();
|
|
9952
10230
|
|
|
@@ -11204,7 +11482,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11204
11482
|
};
|
|
11205
11483
|
const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
|
|
11206
11484
|
const recentFailed = allRuns.filter(
|
|
11207
|
-
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
|
|
11485
|
+
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r) && !isPreStartFailure(r)
|
|
11208
11486
|
);
|
|
11209
11487
|
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
11210
11488
|
candidates.push({
|
|
@@ -11263,6 +11541,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11263
11541
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
11264
11542
|
if (killedByOperator(run)) continue;
|
|
11265
11543
|
if (closedBeforeStart(run)) continue;
|
|
11544
|
+
if (isPreStartFailure(run)) continue;
|
|
11266
11545
|
const repeat = isRepeatDown(run);
|
|
11267
11546
|
candidates.push({
|
|
11268
11547
|
key: `run_failed:${run.id}`,
|
|
@@ -11277,6 +11556,23 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11277
11556
|
})
|
|
11278
11557
|
});
|
|
11279
11558
|
}
|
|
11559
|
+
for (const run of allRuns) {
|
|
11560
|
+
if (run.status !== "failed" || !isSpawnParked(run)) continue;
|
|
11561
|
+
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
11562
|
+
if (closedBeforeStart(run)) continue;
|
|
11563
|
+
candidates.push({
|
|
11564
|
+
key: `spawn_parked:${run.id}`,
|
|
11565
|
+
minDurationMs: 0,
|
|
11566
|
+
// серия попыток УЖЕ прошла свой PRE_START_ATTEMPT_LIMIT — отлёживать нечего
|
|
11567
|
+
build: () => ({
|
|
11568
|
+
runId: run.id,
|
|
11569
|
+
level: "decision",
|
|
11570
|
+
title: runTitle(run, "\u0440\u043E\u0439 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0432\u0437\u044F\u0442\u044C\u0441\u044F \u0437\u0430 \u0437\u0430\u0434\u0430\u0447\u0443"),
|
|
11571
|
+
body: `${run.task_slug}: ${run.stop_reason ?? SPAWN_PARK_MARK}`,
|
|
11572
|
+
...hrefOf(run.task_url)
|
|
11573
|
+
})
|
|
11574
|
+
});
|
|
11575
|
+
}
|
|
11280
11576
|
const queuedCount = allRuns.filter((r) => r.status === "queued").length;
|
|
11281
11577
|
const activeDroneCount2 = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
11282
11578
|
if (queuedCount > 0 && activeDroneCount2 === 0) {
|
|
@@ -11294,6 +11590,31 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11294
11590
|
})
|
|
11295
11591
|
});
|
|
11296
11592
|
}
|
|
11593
|
+
const GROUP_DEP_MISSING = {
|
|
11594
|
+
id: "dep_missing",
|
|
11595
|
+
title: (n) => `${n} ${plural2(n, ["\u043F\u0440\u043E\u0433\u043E\u043D \u0436\u0434\u0451\u0442", "\u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0436\u0434\u0443\u0442", "\u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0436\u0434\u0443\u0442"])} \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435\u0442`
|
|
11596
|
+
};
|
|
11597
|
+
for (const run of allRuns) {
|
|
11598
|
+
if (run.status !== "queued") continue;
|
|
11599
|
+
if (!run.depends_on || run.depends_on.length === 0) continue;
|
|
11600
|
+
const lost = db.depVerdicts(run.depends_on).filter((d) => d.outcome === "unknown");
|
|
11601
|
+
if (!lost.length) continue;
|
|
11602
|
+
candidates.push({
|
|
11603
|
+
key: `dep_missing:${run.task_slug}`,
|
|
11604
|
+
// по СЛАГУ, а не по id зеркала: тот меняется на рестарте (TAS-485)
|
|
11605
|
+
minDurationMs: thresholds.depMissingMs,
|
|
11606
|
+
group: GROUP_DEP_MISSING,
|
|
11607
|
+
build: () => ({
|
|
11608
|
+
runId: run.id,
|
|
11609
|
+
// Пуш: рой сам из этого не выйдет НИКОГДА, а лечение — одно действие человека.
|
|
11610
|
+
// Ровно тот случай, ради которого уровень decision и заведён.
|
|
11611
|
+
level: "decision",
|
|
11612
|
+
title: runTitle(run, "\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435\u0442"),
|
|
11613
|
+
body: `${droneOf(run)}: \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u043E\u0438\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0441 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C\u044E ${joinCapped(lost.map((d) => d.slug))}, \u043D\u043E \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u2014 \u043D\u0438 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435, \u043D\u0438 \u0432 \u043F\u0440\u043E\u0434\u0435. \u0421\u0430\u043C \u043E\u043D \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442: \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{runId, dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E.`,
|
|
11614
|
+
...hrefOf(run.task_url)
|
|
11615
|
+
})
|
|
11616
|
+
});
|
|
11617
|
+
}
|
|
11297
11618
|
if (opts.gateReason && queuedCount > 0) {
|
|
11298
11619
|
const reason = opts.gateReason;
|
|
11299
11620
|
candidates.push({
|
|
@@ -11378,6 +11699,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11378
11699
|
for (const q of db.openQuestions()) {
|
|
11379
11700
|
const run = runsById.get(q.run_id);
|
|
11380
11701
|
if (!run) continue;
|
|
11702
|
+
if (isTerminalStatus(run.status)) continue;
|
|
11381
11703
|
candidates.push({
|
|
11382
11704
|
key: `question_unanswered:${q.id}`,
|
|
11383
11705
|
minDurationMs: thresholds.questionUnansweredMs,
|
|
@@ -11860,22 +12182,22 @@ function makeMergeGateResolver(deps) {
|
|
|
11860
12182
|
}
|
|
11861
12183
|
|
|
11862
12184
|
// src/envGate.ts
|
|
11863
|
-
import { existsSync as
|
|
11864
|
-
import { join as
|
|
12185
|
+
import { existsSync as existsSync14 } from "fs";
|
|
12186
|
+
import { join as join25 } from "path";
|
|
11865
12187
|
import { createConnection } from "net";
|
|
11866
12188
|
import { execa as execa2 } from "execa";
|
|
11867
12189
|
|
|
11868
12190
|
// src/platform.ts
|
|
11869
|
-
import { existsSync as
|
|
11870
|
-
import { delimiter, join as
|
|
12191
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
12192
|
+
import { delimiter, join as join24 } from "path";
|
|
11871
12193
|
function resolveCommandPath(name, env = process.env, platform = process.platform) {
|
|
11872
|
-
if (name.includes("/") || name.includes("\\")) return
|
|
12194
|
+
if (name.includes("/") || name.includes("\\")) return existsSync13(name) ? name : null;
|
|
11873
12195
|
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
11874
12196
|
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
11875
12197
|
for (const dir of dirs) {
|
|
11876
12198
|
for (const ext of exts) {
|
|
11877
|
-
const candidate =
|
|
11878
|
-
if (
|
|
12199
|
+
const candidate = join24(dir, name + ext);
|
|
12200
|
+
if (existsSync13(candidate)) return candidate;
|
|
11879
12201
|
}
|
|
11880
12202
|
}
|
|
11881
12203
|
return null;
|
|
@@ -11958,8 +12280,8 @@ var COMPOSE_MARKERS = [
|
|
|
11958
12280
|
"compose.yaml"
|
|
11959
12281
|
];
|
|
11960
12282
|
function detectRequirements(repo, io = {}) {
|
|
11961
|
-
const exists = io.exists ??
|
|
11962
|
-
const marker = COMPOSE_MARKERS.find((f) => exists(
|
|
12283
|
+
const exists = io.exists ?? existsSync14;
|
|
12284
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join25(repo, f)));
|
|
11963
12285
|
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
11964
12286
|
}
|
|
11965
12287
|
function resolveRequirements(repo, declared, io = {}) {
|
|
@@ -12095,8 +12417,8 @@ import { basename as basename3 } from "path";
|
|
|
12095
12417
|
import { serve as serve2 } from "@hono/node-server";
|
|
12096
12418
|
|
|
12097
12419
|
// src/diff.ts
|
|
12098
|
-
import { statSync as
|
|
12099
|
-
import { join as
|
|
12420
|
+
import { statSync as statSync10 } from "fs";
|
|
12421
|
+
import { join as join26 } from "path";
|
|
12100
12422
|
import { $ as $4 } from "execa";
|
|
12101
12423
|
var MAX_FILES = 200;
|
|
12102
12424
|
var MAX_LINES_PER_FILE = 1500;
|
|
@@ -12239,7 +12561,7 @@ async function resolveBase(wt, baseBranch) {
|
|
|
12239
12561
|
}
|
|
12240
12562
|
function mtime(path) {
|
|
12241
12563
|
try {
|
|
12242
|
-
return
|
|
12564
|
+
return statSync10(path).mtimeMs;
|
|
12243
12565
|
} catch {
|
|
12244
12566
|
return 0;
|
|
12245
12567
|
}
|
|
@@ -12264,7 +12586,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
12264
12586
|
files.push(pf);
|
|
12265
12587
|
}
|
|
12266
12588
|
}
|
|
12267
|
-
files.sort((a, b) => mtime(
|
|
12589
|
+
files.sort((a, b) => mtime(join26(worktree, b.path)) - mtime(join26(worktree, a.path)));
|
|
12268
12590
|
const truncatedFiles = files.length > MAX_FILES;
|
|
12269
12591
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
12270
12592
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -12297,7 +12619,7 @@ async function changedPaths(worktree, opts = {}) {
|
|
|
12297
12619
|
// package.json
|
|
12298
12620
|
var package_default = {
|
|
12299
12621
|
name: "@taskless-app/swarm",
|
|
12300
|
-
version: "0.6.
|
|
12622
|
+
version: "0.6.15",
|
|
12301
12623
|
type: "module",
|
|
12302
12624
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
12303
12625
|
keywords: [
|
|
@@ -12347,7 +12669,7 @@ var package_default = {
|
|
|
12347
12669
|
execa: "^10.0.0",
|
|
12348
12670
|
zod: "3.25.76"
|
|
12349
12671
|
},
|
|
12350
|
-
gitHead: "
|
|
12672
|
+
gitHead: "5bc8e0c067de58fd72b0eb434e80ad29c7a00223"
|
|
12351
12673
|
};
|
|
12352
12674
|
|
|
12353
12675
|
// src/version.ts
|
|
@@ -13980,10 +14302,10 @@ async function checkDaemonVersion(start, opts = {}) {
|
|
|
13980
14302
|
|
|
13981
14303
|
// src/hostCommands.ts
|
|
13982
14304
|
init_src();
|
|
13983
|
-
import { readFileSync as readFileSync18, renameSync as
|
|
13984
|
-
import { join as
|
|
14305
|
+
import { readFileSync as readFileSync18, renameSync as renameSync9, writeFileSync as writeFileSync13 } from "fs";
|
|
14306
|
+
import { join as join27 } from "path";
|
|
13985
14307
|
function hostCommandStatePath(home) {
|
|
13986
|
-
return
|
|
14308
|
+
return join27(home, "host-command.json");
|
|
13987
14309
|
}
|
|
13988
14310
|
function readState(path) {
|
|
13989
14311
|
try {
|
|
@@ -13995,7 +14317,7 @@ function readState(path) {
|
|
|
13995
14317
|
function writeState(path, state) {
|
|
13996
14318
|
try {
|
|
13997
14319
|
writeFileSync13(`${path}.tmp`, JSON.stringify(state));
|
|
13998
|
-
|
|
14320
|
+
renameSync9(`${path}.tmp`, path);
|
|
13999
14321
|
} catch {
|
|
14000
14322
|
}
|
|
14001
14323
|
}
|
|
@@ -14157,8 +14479,8 @@ async function doRestart(cmd, opts, note) {
|
|
|
14157
14479
|
|
|
14158
14480
|
// src/selfUpdate.ts
|
|
14159
14481
|
init_git();
|
|
14160
|
-
import { existsSync as
|
|
14161
|
-
import { join as
|
|
14482
|
+
import { existsSync as existsSync15 } from "fs";
|
|
14483
|
+
import { join as join28 } from "path";
|
|
14162
14484
|
import { $ as $5 } from "execa";
|
|
14163
14485
|
var DAEMON_PACKAGE = "@taskless-app/swarm";
|
|
14164
14486
|
var defaultRun = async (cmd, args, cwd) => {
|
|
@@ -14171,7 +14493,7 @@ function tail(text, max = 400) {
|
|
|
14171
14493
|
}
|
|
14172
14494
|
async function runSelfUpdate(io) {
|
|
14173
14495
|
const run = io.run ?? defaultRun;
|
|
14174
|
-
const exists = io.exists ??
|
|
14496
|
+
const exists = io.exists ?? existsSync15;
|
|
14175
14497
|
const repo = io.repo;
|
|
14176
14498
|
await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
|
|
14177
14499
|
const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
|
|
@@ -14200,7 +14522,7 @@ async function runSelfUpdate(io) {
|
|
|
14200
14522
|
};
|
|
14201
14523
|
}
|
|
14202
14524
|
await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
|
|
14203
|
-
if (!exists(
|
|
14525
|
+
if (!exists(join28(repo, "pnpm-lock.yaml"))) {
|
|
14204
14526
|
return { ok: false, step: "install", message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 pnpm-lock.yaml \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u0435\u0433\u043E \u044D\u0442\u0438\u043C \u043F\u0443\u0442\u0451\u043C \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C" };
|
|
14205
14527
|
}
|
|
14206
14528
|
const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
|
|
@@ -14258,6 +14580,47 @@ function makeOvermindNarrator() {
|
|
|
14258
14580
|
};
|
|
14259
14581
|
}
|
|
14260
14582
|
|
|
14583
|
+
// src/orphanQuestions.ts
|
|
14584
|
+
init_src();
|
|
14585
|
+
var ORPHAN_NOTE_WINDOW_MS = DAY_MS;
|
|
14586
|
+
var ORPHAN_NOTE_PREFIX = "\u0412\u043E\u043F\u0440\u043E\u0441 \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u044C\u0448\u0435";
|
|
14587
|
+
function orphanWithdrawReason(status) {
|
|
14588
|
+
return `\u0441\u043D\u044F\u0442 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438: \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (${status}), \u043E\u0442\u0432\u0435\u0447\u0430\u0442\u044C \u043D\u0435\u043A\u043E\u043C\u0443`;
|
|
14589
|
+
}
|
|
14590
|
+
function reapOrphanedQuestions(db, now, windowMs = ORPHAN_NOTE_WINDOW_MS) {
|
|
14591
|
+
const result = { withdrawn: 0, noted: 0 };
|
|
14592
|
+
let open;
|
|
14593
|
+
let runsById;
|
|
14594
|
+
try {
|
|
14595
|
+
open = db.openQuestions();
|
|
14596
|
+
if (open.length === 0) return result;
|
|
14597
|
+
runsById = new Map(db.listRuns().map((r) => [r.id, r]));
|
|
14598
|
+
} catch {
|
|
14599
|
+
return result;
|
|
14600
|
+
}
|
|
14601
|
+
for (const q of open) {
|
|
14602
|
+
const run = runsById.get(q.run_id);
|
|
14603
|
+
if (!run) continue;
|
|
14604
|
+
if (!isTerminalStatus(run.status) && run.superseded_by == null) continue;
|
|
14605
|
+
try {
|
|
14606
|
+
if (now - sqliteMs(run.updated_at) <= windowMs) {
|
|
14607
|
+
db.addEvent(run.id, NOTE_KIND, orphanNoteText(run, q), NOTE_ACTOR);
|
|
14608
|
+
result.noted++;
|
|
14609
|
+
}
|
|
14610
|
+
db.withdrawQuestion(q.id, orphanWithdrawReason(run.status));
|
|
14611
|
+
result.withdrawn++;
|
|
14612
|
+
} catch (e) {
|
|
14613
|
+
console.error(`[orphan-questions] \u0432\u043E\u043F\u0440\u043E\u0441 #${q.id} \u043D\u0435 \u0441\u043D\u044F\u0442, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
|
|
14614
|
+
}
|
|
14615
|
+
}
|
|
14616
|
+
return result;
|
|
14617
|
+
}
|
|
14618
|
+
var NOTE_KIND = "note";
|
|
14619
|
+
var NOTE_ACTOR = "orchestrator";
|
|
14620
|
+
function orphanNoteText(run, q) {
|
|
14621
|
+
return `${ORPHAN_NOTE_PREFIX} (${run.status}). \u041E\u0442\u0432\u0435\u0447\u0430\u0442\u044C \u043D\u0435\u043A\u043E\u043C\u0443, \u0437\u0432\u0430\u0442\u044C \u043D\u0438\u043A\u043E\u0433\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u043C \u2014 \u0442\u0435\u043A\u0441\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u044E: \xAB${q.text}\xBB`;
|
|
14622
|
+
}
|
|
14623
|
+
|
|
14261
14624
|
// src/probe.ts
|
|
14262
14625
|
init_executors();
|
|
14263
14626
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -14318,18 +14681,18 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
14318
14681
|
}
|
|
14319
14682
|
|
|
14320
14683
|
// src/anomalyState.ts
|
|
14321
|
-
import { existsSync as
|
|
14322
|
-
import { join as
|
|
14684
|
+
import { existsSync as existsSync16, readFileSync as readFileSync19, renameSync as renameSync10, writeFileSync as writeFileSync14 } from "fs";
|
|
14685
|
+
import { join as join29 } from "path";
|
|
14323
14686
|
var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
14324
14687
|
var MAX_RESOLVED = 200;
|
|
14325
14688
|
function emptyState() {
|
|
14326
14689
|
return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
|
|
14327
14690
|
}
|
|
14328
14691
|
function anomalyStatePath(home) {
|
|
14329
|
-
return
|
|
14692
|
+
return join29(home, "anomaly-state.json");
|
|
14330
14693
|
}
|
|
14331
14694
|
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
14332
|
-
const exists = io.exists ??
|
|
14695
|
+
const exists = io.exists ?? existsSync16;
|
|
14333
14696
|
const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
|
|
14334
14697
|
const path = anomalyStatePath(home);
|
|
14335
14698
|
if (!exists(path)) return emptyState();
|
|
@@ -14361,7 +14724,7 @@ function saveAnomalyState(home, state, io = {}) {
|
|
|
14361
14724
|
const path = anomalyStatePath(home);
|
|
14362
14725
|
const writeFile3 = io.writeFile ?? ((p, data) => {
|
|
14363
14726
|
writeFileSync14(`${p}.tmp`, data);
|
|
14364
|
-
|
|
14727
|
+
renameSync10(`${p}.tmp`, p);
|
|
14365
14728
|
});
|
|
14366
14729
|
writeFile3(path, JSON.stringify(state, null, 2));
|
|
14367
14730
|
}
|
|
@@ -14687,6 +15050,9 @@ function short(files, n = 3) {
|
|
|
14687
15050
|
return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
|
|
14688
15051
|
}
|
|
14689
15052
|
var SPINE_PREDICTION_WEIGHT = 0.7;
|
|
15053
|
+
function spineOfPrediction(spine, predicted) {
|
|
15054
|
+
return predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
|
|
15055
|
+
}
|
|
14690
15056
|
function decideConflict({ model, candidate, active }) {
|
|
14691
15057
|
if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
|
|
14692
15058
|
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
@@ -14694,23 +15060,16 @@ function decideConflict({ model, candidate, active }) {
|
|
|
14694
15060
|
if (!predicted.length) return { kind: "unknown" };
|
|
14695
15061
|
if (!active.length) return { kind: "clear" };
|
|
14696
15062
|
const spine = new Set(model.spine);
|
|
14697
|
-
const candidateSpine =
|
|
14698
|
-
if (candidateSpine.
|
|
14699
|
-
const
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
message: `\u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(candidateSpine)}) \u2014 \u0442\u0430\u043A\u0438\u0435 \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B; \u0436\u0434\u0443 ${blocker.run.task_slug} (#${blocker.run.id})`
|
|
14704
|
-
};
|
|
14705
|
-
}
|
|
14706
|
-
for (const w of active) {
|
|
14707
|
-
if (!w.actual) continue;
|
|
14708
|
-
const theirSpine = w.files.filter((f) => spine.has(f));
|
|
14709
|
-
if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
|
|
15063
|
+
const candidateSpine = new Set(spineOfPrediction(spine, predicted));
|
|
15064
|
+
if (candidateSpine.size) {
|
|
15065
|
+
for (const w of active) {
|
|
15066
|
+
const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run));
|
|
15067
|
+
const shared = theirSpine.filter((f) => candidateSpine.has(f));
|
|
15068
|
+
if (!shared.length) continue;
|
|
14710
15069
|
return {
|
|
14711
15070
|
kind: "wait",
|
|
14712
15071
|
blockedBy: w.run,
|
|
14713
|
-
message:
|
|
15072
|
+
message: `\u044D\u0442\u0438 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0440\u043E\u0433\u0430\u044E\u0442 \u043E\u0434\u043D\u0438 \u0444\u0430\u0439\u043B\u044B \u043E\u0431\u0449\u0435\u0433\u043E \u0445\u0440\u0435\u0431\u0442\u0430 (${short(shared)}) \u2014 \u0442\u0430\u043A\u0438\u0435 \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B: \u0436\u0434\u0443 ${w.run.task_slug} (#${w.run.id})` + (w.actual ? "" : " (\u043F\u043E \u043F\u0440\u0435\u0434\u0441\u043A\u0430\u0437\u0430\u043D\u0438\u044E \u2014 \u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0447\u0430\u043B \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434)")
|
|
14714
15073
|
};
|
|
14715
15074
|
}
|
|
14716
15075
|
}
|
|
@@ -15322,7 +15681,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
15322
15681
|
}
|
|
15323
15682
|
return parts.join("\n");
|
|
15324
15683
|
}
|
|
15325
|
-
var HERO_CHARTER_PATH =
|
|
15684
|
+
var HERO_CHARTER_PATH = join30(dirname4(THIS_FILE), "..", "docs", "HERO.md");
|
|
15326
15685
|
function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
|
|
15327
15686
|
const name = run.name || `#${run.id}`;
|
|
15328
15687
|
const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
|
|
@@ -15622,7 +15981,7 @@ async function wakeRun(deps, runId) {
|
|
|
15622
15981
|
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` };
|
|
15623
15982
|
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` };
|
|
15624
15983
|
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` };
|
|
15625
|
-
const worktreeExists = deps.worktreeExists ??
|
|
15984
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
15626
15985
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
15627
15986
|
const wt = run.worktree_path;
|
|
15628
15987
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -15707,9 +16066,9 @@ function repoHasCi(deps, run) {
|
|
|
15707
16066
|
function repoHasCiWorkflows(worktreePath, repo) {
|
|
15708
16067
|
for (const base3 of [worktreePath, repo]) {
|
|
15709
16068
|
if (!base3) continue;
|
|
15710
|
-
const dir =
|
|
16069
|
+
const dir = join30(base3, ".github", "workflows");
|
|
15711
16070
|
try {
|
|
15712
|
-
if (
|
|
16071
|
+
if (readdirSync13(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
|
|
15713
16072
|
} catch {
|
|
15714
16073
|
}
|
|
15715
16074
|
}
|
|
@@ -15858,7 +16217,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
|
|
|
15858
16217
|
return true;
|
|
15859
16218
|
}
|
|
15860
16219
|
function worktreeAndTranscriptAlive(deps, worktree) {
|
|
15861
|
-
const exists = deps.worktreeExists ??
|
|
16220
|
+
const exists = deps.worktreeExists ?? existsSync17;
|
|
15862
16221
|
const transcript = deps.hasTranscript ?? executorHasSession;
|
|
15863
16222
|
return exists(worktree) && transcript(worktree);
|
|
15864
16223
|
}
|
|
@@ -16161,6 +16520,15 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16161
16520
|
// причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
|
|
16162
16521
|
});
|
|
16163
16522
|
}
|
|
16523
|
+
const spentAttempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
|
|
16524
|
+
if (spentAttempts >= PRE_START_ATTEMPT_LIMIT) {
|
|
16525
|
+
const why = `${PRE_START_MARK} ${spentAttempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-422)`;
|
|
16526
|
+
deps.db.updateRun(run.id, { status: "stopped", stop_reason: why });
|
|
16527
|
+
deps.db.addEvent(run.id, "step", why);
|
|
16528
|
+
const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), `pre-start-exhausted:${run.task_slug}`, why, GATE_LOG_HEARTBEAT_TICKS);
|
|
16529
|
+
if (d.log) console.error(`[overmind] ${run.task_slug}: ${why}${d.changed ? "" : ` \u2014 \u0435\u0449\u0451 \u0432\u0438\u0441\u0438\u0442, \u0442\u0438\u043A ${d.ticks}`}`);
|
|
16530
|
+
return "closed";
|
|
16531
|
+
}
|
|
16164
16532
|
const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
16165
16533
|
if (mergedUrl) {
|
|
16166
16534
|
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
@@ -16171,7 +16539,11 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16171
16539
|
}
|
|
16172
16540
|
const depVerdicts = deps.db.depVerdicts(run.depends_on);
|
|
16173
16541
|
const unmet = depVerdicts.filter((d) => !d.met);
|
|
16174
|
-
if (unmet.length)
|
|
16542
|
+
if (unmet.length) {
|
|
16543
|
+
const lost = unmet.filter((d) => d.outcome === "unknown");
|
|
16544
|
+
const why = lost.length ? `\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u041D\u0415\u0422: ${lost.map((d) => d.slug).join(", ")} \u2014 \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430; \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442, \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E (TASWM-399)` : `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`;
|
|
16545
|
+
return deferSpawn(deps, run, why);
|
|
16546
|
+
}
|
|
16175
16547
|
if (!await envGateAllows(deps, run)) return "skipped";
|
|
16176
16548
|
if (!await projectQuotaGateAllows(deps, run)) return "skipped";
|
|
16177
16549
|
const prodOverride = await prodOverrideReason(deps, run, ["queued"]);
|
|
@@ -16181,6 +16553,8 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16181
16553
|
}
|
|
16182
16554
|
noteExhaustedDeps(deps, run, depVerdicts);
|
|
16183
16555
|
await fillSuggestedArea(deps, run);
|
|
16556
|
+
const repoGap = unresolvedRepoReason(run.repo, deps.reposServed ? deps.reposServed() : []);
|
|
16557
|
+
if (repoGap) return await failBeforeStart(deps, run, repoGap);
|
|
16184
16558
|
try {
|
|
16185
16559
|
await deps.spawn(run);
|
|
16186
16560
|
if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
|
|
@@ -16197,10 +16571,32 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16197
16571
|
return "skipped";
|
|
16198
16572
|
}
|
|
16199
16573
|
if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
|
|
16200
|
-
deps
|
|
16201
|
-
|
|
16574
|
+
return await failBeforeStart(deps, run, String(e));
|
|
16575
|
+
}
|
|
16576
|
+
}
|
|
16577
|
+
async function failBeforeStart(deps, run, reason) {
|
|
16578
|
+
deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
|
|
16579
|
+
deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
|
|
16580
|
+
const attempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
|
|
16581
|
+
if (attempts < PRE_START_ATTEMPT_LIMIT) {
|
|
16582
|
+
narrate(deps, `${droneLabel(run)} \u2014 ${PRE_START_MARK} (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempts} \u0438\u0437 ${PRE_START_ATTEMPT_LIMIT}): ${reason}`);
|
|
16202
16583
|
return "failed";
|
|
16203
16584
|
}
|
|
16585
|
+
const parked = parkReason(attempts, reason);
|
|
16586
|
+
deps.db.updateRun(run.id, { stop_reason: parked });
|
|
16587
|
+
deps.db.addEvent(run.id, "step", parked);
|
|
16588
|
+
narrate(deps, `${run.task_slug}: ${parked}`);
|
|
16589
|
+
if (deps.parkTask) {
|
|
16590
|
+
const ok = await deps.parkTask(run, parked).catch(() => false);
|
|
16591
|
+
if (!ok) {
|
|
16592
|
+
deps.db.addEvent(
|
|
16593
|
+
run.id,
|
|
16594
|
+
"step",
|
|
16595
|
+
`${SPAWN_PARK_MARK}: \u0443\u0432\u0435\u0441\u0442\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u0442\u0441\u044F) \u2014 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u0435\u0451 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u0442, \u043D\u043E \u0443\u0431\u0440\u0430\u0442\u044C \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043F\u0440\u0438\u0434\u0451\u0442\u0441\u044F \u0440\u0443\u043A\u0430\u043C\u0438`
|
|
16596
|
+
);
|
|
16597
|
+
}
|
|
16598
|
+
}
|
|
16599
|
+
return "failed";
|
|
16204
16600
|
}
|
|
16205
16601
|
async function batchSpawnQueue(deps, droneCap) {
|
|
16206
16602
|
for (const run of orderQueue(deps.db.listRuns("queued"))) {
|
|
@@ -16222,7 +16618,7 @@ async function resumeWatchdogParked(deps, droneCap) {
|
|
|
16222
16618
|
}
|
|
16223
16619
|
if (!candidate) return;
|
|
16224
16620
|
if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
|
|
16225
|
-
const worktreeExists = deps.worktreeExists ??
|
|
16621
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
16226
16622
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
16227
16623
|
const wt = candidate.worktree_path;
|
|
16228
16624
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -16391,7 +16787,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
16391
16787
|
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
16392
16788
|
if (!deps.respawn) return;
|
|
16393
16789
|
const wt = resumeCandidate.worktree_path;
|
|
16394
|
-
const worktreeExists = deps.worktreeExists ??
|
|
16790
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
16395
16791
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
16396
16792
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
16397
16793
|
const mode = canContinue ? "continue" : "fresh";
|
|
@@ -16660,7 +17056,7 @@ function buildEscalationCandidates(db) {
|
|
|
16660
17056
|
}
|
|
16661
17057
|
async function tickOnce(deps) {
|
|
16662
17058
|
const now = deps.now ? deps.now() : Date.now();
|
|
16663
|
-
const worktreeExists = deps.worktreeExists ??
|
|
17059
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
16664
17060
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
16665
17061
|
deps.sampleHost?.();
|
|
16666
17062
|
if (deps.rehydrate) {
|
|
@@ -16991,6 +17387,13 @@ async function tickOnce(deps) {
|
|
|
16991
17387
|
liveChildPpids = null;
|
|
16992
17388
|
logTransientSpawnError("ps (livePpids)", e);
|
|
16993
17389
|
}
|
|
17390
|
+
if (deps.sweepCost) {
|
|
17391
|
+
try {
|
|
17392
|
+
deps.sweepCost();
|
|
17393
|
+
} catch (e) {
|
|
17394
|
+
console.error("[overmind] \u0441\u0432\u0438\u043F \u0443\u0447\u0451\u0442\u0430 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u0438 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
17395
|
+
}
|
|
17396
|
+
}
|
|
16994
17397
|
for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
|
|
16995
17398
|
const alive = run.pid != null && deps.isAlive(run.pid);
|
|
16996
17399
|
if (!alive && deps.db.takeRun && !await deps.db.takeRun(run.id)) continue;
|
|
@@ -17216,6 +17619,16 @@ async function tickOnce(deps) {
|
|
|
17216
17619
|
if (deps.respawn && deps.resourcesOk && !stormActive) {
|
|
17217
17620
|
await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
|
|
17218
17621
|
}
|
|
17622
|
+
try {
|
|
17623
|
+
const reaped = reapOrphanedQuestions(deps.db, now);
|
|
17624
|
+
if (reaped.withdrawn > 0) {
|
|
17625
|
+
console.log(
|
|
17626
|
+
`[overmind] \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B: \u0441\u043D\u044F\u0442\u043E ${reaped.withdrawn}, \u0437\u0430\u043C\u0435\u0442\u043A\u043E\u0439 \u0432 \u043B\u0435\u043D\u0442\u0443 \u0443\u0448\u043B\u043E ${reaped.noted}`
|
|
17627
|
+
);
|
|
17628
|
+
}
|
|
17629
|
+
} catch (e) {
|
|
17630
|
+
console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
|
|
17631
|
+
}
|
|
17219
17632
|
if (deps.anomalies && !deps.control?.paused) {
|
|
17220
17633
|
const az = deps.anomalies;
|
|
17221
17634
|
let ready = [];
|
|
@@ -17367,7 +17780,7 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
17367
17780
|
model: reviewRun.model,
|
|
17368
17781
|
review_model: reviewRun.review_model
|
|
17369
17782
|
});
|
|
17370
|
-
const wt =
|
|
17783
|
+
const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
|
|
17371
17784
|
try {
|
|
17372
17785
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
17373
17786
|
await deps.git.setWorktreeIdentity(wt, resolver.name);
|
|
@@ -17424,7 +17837,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
17424
17837
|
// (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
|
|
17425
17838
|
workspace_id: reviewRun.workspace_id
|
|
17426
17839
|
});
|
|
17427
|
-
const wt =
|
|
17840
|
+
const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
17428
17841
|
try {
|
|
17429
17842
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
17430
17843
|
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
@@ -17499,7 +17912,7 @@ async function spawnVerifier(deps, verifiedRun, files) {
|
|
|
17499
17912
|
// штамп цикла: фикс-итерацию проверяет свежий верификатор
|
|
17500
17913
|
workspace_id: verifiedRun.workspace_id
|
|
17501
17914
|
});
|
|
17502
|
-
const wt =
|
|
17915
|
+
const wt = join30(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
|
|
17503
17916
|
try {
|
|
17504
17917
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
17505
17918
|
await deps.git.setWorktreeIdentity(wt, verifier.name);
|
|
@@ -17537,7 +17950,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
17537
17950
|
]);
|
|
17538
17951
|
const base3 = effectiveBaseBranch(run.base_branch, projectBase);
|
|
17539
17952
|
const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
|
|
17540
|
-
if (!
|
|
17953
|
+
if (!existsSync17(wt)) {
|
|
17541
17954
|
const branch = runToSpawn.branch || runToSpawn.task_slug;
|
|
17542
17955
|
const source = await addWorktree(repo, wt, branch, {
|
|
17543
17956
|
...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
|
|
@@ -17564,7 +17977,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
17564
17977
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
17565
17978
|
}
|
|
17566
17979
|
async function syncCrashedOnStartup(deps) {
|
|
17567
|
-
const worktreeExists = deps.worktreeExists ??
|
|
17980
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
17568
17981
|
let orphaned = 0;
|
|
17569
17982
|
let merged = 0;
|
|
17570
17983
|
let requeued = 0;
|
|
@@ -17653,10 +18066,12 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
17653
18066
|
roots: scanRootsList,
|
|
17654
18067
|
maxDepth: SWARM_SCAN_DEPTH,
|
|
17655
18068
|
cachePath: REPO_MAP_PATH,
|
|
18069
|
+
// TASWM-418: кэш обязан протухать — иначе новый клон на машине не находится никогда.
|
|
18070
|
+
scanTtlMs: SWARM_REPO_SCAN_TTL_MS,
|
|
17656
18071
|
state: repoMap,
|
|
17657
18072
|
log: (m) => console.log(m)
|
|
17658
18073
|
});
|
|
17659
|
-
|
|
18074
|
+
let repoRegistry = await rebuildRegistry();
|
|
17660
18075
|
if (repoRegistry.length === 0) {
|
|
17661
18076
|
console.warn(
|
|
17662
18077
|
`[overmind] \u041D\u0415 \u041D\u0410\u0428\u0401\u041B \u041D\u0418 \u041E\u0414\u041D\u041E\u0413\u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442: \u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u0435\u043C owner/repo, \u0430 \u0441\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0441 \u0447\u0435\u043C. \u0418\u0441\u043A\u0430\u043B \u0432: ${scanRootsList.slice(0, 6).join(", ")}. ` + // TASWM-336: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
|
|
@@ -17686,8 +18101,13 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
17686
18101
|
// Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
|
|
17687
18102
|
// и список для claim/heartbeat, и обратную карту owner/repo → путь.
|
|
17688
18103
|
refreshRepoMap: async () => {
|
|
17689
|
-
|
|
17690
|
-
|
|
18104
|
+
repoRegistry = await rebuildRegistry();
|
|
18105
|
+
client.setRepoRegistry(repoRegistry);
|
|
18106
|
+
},
|
|
18107
|
+
// TASWM-422: что машина обслуживает ПРЯМО СЕЙЧАС — читается из ЖИВОГО реестра (он
|
|
18108
|
+
// пересобирается тиком выше), чтобы причина «клона нет на этой машине» называла
|
|
18109
|
+
// актуальный список, а не снимок на старте демона.
|
|
18110
|
+
reposServed: () => repoRegistry.map((e) => e.repoFullName)
|
|
17691
18111
|
};
|
|
17692
18112
|
}
|
|
17693
18113
|
var crashGuardsInstalled = false;
|
|
@@ -17707,22 +18127,22 @@ async function migrateWorktreesAtStartup() {
|
|
|
17707
18127
|
legacyDir: WORKTREES_LEGACY_DIR,
|
|
17708
18128
|
targetDir: WORKTREES_DIR,
|
|
17709
18129
|
envOverride: process.env.SWARM_WORKTREES || "",
|
|
17710
|
-
exists: (d) =>
|
|
18130
|
+
exists: (d) => existsSync17(d),
|
|
17711
18131
|
isNonEmptyDir: (d) => {
|
|
17712
18132
|
try {
|
|
17713
|
-
return
|
|
18133
|
+
return readdirSync13(d).length > 0;
|
|
17714
18134
|
} catch {
|
|
17715
18135
|
return false;
|
|
17716
18136
|
}
|
|
17717
18137
|
},
|
|
17718
18138
|
liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
|
|
17719
|
-
rename: (from, to) =>
|
|
18139
|
+
rename: (from, to) => renameSync11(from, to),
|
|
17720
18140
|
repairMoved: (t) => repairMovedWorktrees(t),
|
|
17721
18141
|
log: (m) => console.log(m)
|
|
17722
18142
|
});
|
|
17723
18143
|
} catch (e) {
|
|
17724
18144
|
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);
|
|
17725
|
-
return
|
|
18145
|
+
return existsSync17(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
|
|
17726
18146
|
}
|
|
17727
18147
|
}
|
|
17728
18148
|
async function reportWorkspaceAccess(tl, db) {
|
|
@@ -17742,6 +18162,14 @@ async function runDaemon(opts) {
|
|
|
17742
18162
|
const daemonStart = captureDaemonStart();
|
|
17743
18163
|
const latestRelease = fetchLatestVersion();
|
|
17744
18164
|
const worktreesDir = await migrateWorktreesAtStartup();
|
|
18165
|
+
{
|
|
18166
|
+
const swept = sweepLegacyRunLogs(RUN_LOGS_DIR);
|
|
18167
|
+
if (swept.moved || swept.failed) {
|
|
18168
|
+
console.log(
|
|
18169
|
+
`[overmind] run-logs: ${swept.moved} \u043A\u0430\u0442\u0430\u043B\u043E\u0433(\u043E\u0432) \u0441\u0442\u0430\u0440\u043E\u0439 \u0440\u0430\u0441\u043A\u043B\u0430\u0434\u043A\u0438 (\u0430\u0434\u0440\u0435\u0441\u0430\u0446\u0438\u044F \u043D\u043E\u043C\u0435\u0440\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0430) \u0443\u0431\u0440\u0430\u043D\u043E \u0432 ${swept.legacyDir}` + (swept.failed ? `, \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438: ${swept.failed}` : "")
|
|
18170
|
+
);
|
|
18171
|
+
}
|
|
18172
|
+
}
|
|
17745
18173
|
if (!opts.repo) {
|
|
17746
18174
|
const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
|
|
17747
18175
|
if (remembered) opts.repo = remembered;
|
|
@@ -17752,7 +18180,8 @@ async function runDaemon(opts) {
|
|
|
17752
18180
|
host,
|
|
17753
18181
|
repoMap: repoMapState,
|
|
17754
18182
|
refreshRepoMap: rebuildRepoMap,
|
|
17755
|
-
prodMcp
|
|
18183
|
+
prodMcp,
|
|
18184
|
+
reposServed
|
|
17756
18185
|
} = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
|
|
17757
18186
|
await preflightAuth();
|
|
17758
18187
|
const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
|
|
@@ -17798,6 +18227,8 @@ async function runDaemon(opts) {
|
|
|
17798
18227
|
overmindEntry: THIS_FILE,
|
|
17799
18228
|
dbPath: DB_PATH,
|
|
17800
18229
|
runLogsDir: RUN_LOGS_DIR,
|
|
18230
|
+
// TASWM-419: чем именуется каталог лога прогона — стабильные ключи из стора, не номер.
|
|
18231
|
+
runLogKeys: (run) => db.runLogKeys(run.id),
|
|
17801
18232
|
// Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
|
|
17802
18233
|
executor: EXECUTOR,
|
|
17803
18234
|
droneIsolation: DRONE_ISOLATION,
|
|
@@ -17865,6 +18296,7 @@ async function runDaemon(opts) {
|
|
|
17865
18296
|
lastOwnCapacity,
|
|
17866
18297
|
hostResourcesForProd(),
|
|
17867
18298
|
{ active: stormActive, reason: stormStartedAt ? `\u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 ${stormStartedAt} UTC` : null },
|
|
18299
|
+
worktreesDir,
|
|
17868
18300
|
// TASWM-393: ВХОД в Taskless — та же природа, что пауза и шторм: состояние
|
|
17869
18301
|
// машины, о котором знает только она. Пульс идёт и без входа, поэтому «на
|
|
17870
18302
|
// связи» без этого поля читалось как «возьмёт работу» (12.08 — шесть прогонов
|
|
@@ -17883,7 +18315,13 @@ async function runDaemon(opts) {
|
|
|
17883
18315
|
{
|
|
17884
18316
|
runLogsDir: RUN_LOGS_DIR,
|
|
17885
18317
|
daemonLogPath: DAEMON_LOG_PATH,
|
|
17886
|
-
|
|
18318
|
+
// TASWM-419: каталог лога адресуется стабильными ключами прогона, а не его
|
|
18319
|
+
// локальным номером — номера переиспользуются между жизнями зеркала, и
|
|
18320
|
+
// оператору приезжал хвост чужого прогона.
|
|
18321
|
+
runLogKeys: (prodRunId) => {
|
|
18322
|
+
const local = client.localRunIdForProd(prodRunId);
|
|
18323
|
+
return local == null ? [] : db.runLogKeys(local);
|
|
18324
|
+
},
|
|
17887
18325
|
deliver: (requestId, payload) => client.deliverLog(requestId, payload)
|
|
17888
18326
|
},
|
|
17889
18327
|
reply.logRequests
|
|
@@ -17941,6 +18379,7 @@ async function runDaemon(opts) {
|
|
|
17941
18379
|
questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
|
|
17942
18380
|
outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS,
|
|
17943
18381
|
resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
|
|
18382
|
+
depMissingMs: SWARM_ANOMALY_DEP_MISSING_MS,
|
|
17944
18383
|
runDownWindowMs: SWARM_ANOMALY_RUN_DOWN_WINDOW_MS
|
|
17945
18384
|
},
|
|
17946
18385
|
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
@@ -18200,6 +18639,30 @@ async function runDaemon(opts) {
|
|
|
18200
18639
|
taskClosed: (slug, ws) => tl.forWorkspace(ws).getTask(slug).then((t) => t ? isClosingTaskCategory(t.statusCategory) : null)
|
|
18201
18640
|
} : {},
|
|
18202
18641
|
finishTask: (slug, ws) => tlFor(ws)?.finishTask(slug) ?? Promise.resolve(),
|
|
18642
|
+
...reposServed ? { reposServed } : {},
|
|
18643
|
+
// TASWM-422: ОТЛОЖИТЬ задачу, по которой рой трижды не сумел поднять дрона. Механика —
|
|
18644
|
+
// ровно та же, что у return_task дрона (TASWM-412): `claimableTaskWhere` (api) требует
|
|
18645
|
+
// категорию unstarted, и start_task её ломает — задача перестаёт быть claimable, то есть
|
|
18646
|
+
// авто-интейк больше не берёт её раз в минуту. Успех проверяем ФАКТОМ (перечитываем
|
|
18647
|
+
// задачу), а не кодом возврата: клиент глушит отказ сервера внутрь (safeCall), и по
|
|
18648
|
+
// возврату «отложил» было бы неотличимо от «сказал, но не доехало» — то самое молчание,
|
|
18649
|
+
// из-за которого 17.08 круг и крутился.
|
|
18650
|
+
parkTask: async (run, reason) => {
|
|
18651
|
+
const client = tlFor(run.workspace_id);
|
|
18652
|
+
if (!client) return false;
|
|
18653
|
+
await client.addComment(
|
|
18654
|
+
run.task_slug,
|
|
18655
|
+
`${reason}
|
|
18656
|
+
|
|
18657
|
+
\u0417\u0430\u0434\u0430\u0447\u0430 \u0441\u043D\u044F\u0442\u0430 \u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u043E\u044F \u2014 \u043F\u043E\u043A\u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043D\u0435 \u0443\u0441\u0442\u0440\u0430\u043D\u0435\u043D\u0430, \u0431\u0440\u0430\u0442\u044C \u0435\u0451 \u0441\u043D\u043E\u0432\u0430 \u0441\u043C\u044B\u0441\u043B\u0430 \u043D\u0435\u0442. \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0438 \u0432\u0435\u0440\u043D\u0438 \u0432 \u0431\u044D\u043A\u043B\u043E\u0433.`,
|
|
18658
|
+
{ swarmKind: "report", swarmDroneName: run.name }
|
|
18659
|
+
).catch(() => {
|
|
18660
|
+
});
|
|
18661
|
+
await client.startTask(run.task_slug).catch(() => {
|
|
18662
|
+
});
|
|
18663
|
+
const after = await client.getTask(run.task_slug).catch(() => null);
|
|
18664
|
+
return after != null && after.statusCategory !== "unstarted";
|
|
18665
|
+
},
|
|
18203
18666
|
// TASWM-272: отчёт/вердикт-компенсация при усыновлении мёртвого дрона — реальный
|
|
18204
18667
|
// TasklessClient.addComment + gh/git хелперы. Без tl (нет токена) ключи не задаём —
|
|
18205
18668
|
// postAdoptionReport/postScrapedVerdictReport становятся no-op, как раньше.
|
|
@@ -18221,9 +18684,12 @@ async function runDaemon(opts) {
|
|
|
18221
18684
|
// TASWM-364: swarmModels — ТОТ ЖЕ снимок, что уехал в launcher: цена токенов считается
|
|
18222
18685
|
// по модели, на которой прогон реально работал.
|
|
18223
18686
|
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels }),
|
|
18687
|
+
// TASWM-413: тот же аккаунтант, но поводом служит завершённый ЗАПУСК, а не смерть
|
|
18688
|
+
// прогона в running — иначе сданный через done() прогон не получает цену никогда.
|
|
18689
|
+
sweepCost: makeCostSweeper({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels, isAlive: pidAlive }),
|
|
18224
18690
|
// Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
|
|
18225
18691
|
// (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
|
|
18226
|
-
exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
|
|
18692
|
+
exitOutcome: makeExitClassifier(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
|
|
18227
18693
|
// Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
|
|
18228
18694
|
livePpids,
|
|
18229
18695
|
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
@@ -18370,7 +18836,7 @@ async function runDaemon(opts) {
|
|
|
18370
18836
|
mode,
|
|
18371
18837
|
nudge
|
|
18372
18838
|
),
|
|
18373
|
-
worktreeExists: (p) =>
|
|
18839
|
+
worktreeExists: (p) => existsSync17(p),
|
|
18374
18840
|
hasTranscript: executorHasSession,
|
|
18375
18841
|
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
18376
18842
|
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|