@taskless-app/swarm 0.6.13 → 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 +496 -200
- package/dist/overmind.js +955 -361
- 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
|
|
|
@@ -425,6 +432,40 @@ var init_mergeGate = __esm({
|
|
|
425
432
|
}
|
|
426
433
|
});
|
|
427
434
|
|
|
435
|
+
// ../swarm-core/src/modelPolicy.ts
|
|
436
|
+
function findProvider(id) {
|
|
437
|
+
const key = typeof id === "string" ? id.trim().toLowerCase() : "";
|
|
438
|
+
return MODEL_PROVIDERS.find((p) => p.id === key) ?? null;
|
|
439
|
+
}
|
|
440
|
+
function parseModelRef(ref) {
|
|
441
|
+
const s = typeof ref === "string" ? ref.trim() : "";
|
|
442
|
+
if (!s) return null;
|
|
443
|
+
const slash = s.indexOf("/");
|
|
444
|
+
if (slash < 0) return { providerId: DEFAULT_PROVIDER_ID, model: s };
|
|
445
|
+
const providerId = s.slice(0, slash).trim().toLowerCase();
|
|
446
|
+
const model = s.slice(slash + 1).trim();
|
|
447
|
+
if (!model) return null;
|
|
448
|
+
return { providerId: providerId || DEFAULT_PROVIDER_ID, model };
|
|
449
|
+
}
|
|
450
|
+
function defaultModelFor(role, providerId = null) {
|
|
451
|
+
const provider = findProvider(providerId) ?? findProvider(DEFAULT_PROVIDER_ID);
|
|
452
|
+
return provider.roles[role];
|
|
453
|
+
}
|
|
454
|
+
var MODEL_PROVIDERS, DEFAULT_PROVIDER_ID;
|
|
455
|
+
var init_modelPolicy = __esm({
|
|
456
|
+
"../swarm-core/src/modelPolicy.ts"() {
|
|
457
|
+
"use strict";
|
|
458
|
+
MODEL_PROVIDERS = [
|
|
459
|
+
{
|
|
460
|
+
id: "anthropic",
|
|
461
|
+
label: "Anthropic",
|
|
462
|
+
roles: { executor: "claude-opus-5", review: "claude-sonnet-5" }
|
|
463
|
+
}
|
|
464
|
+
];
|
|
465
|
+
DEFAULT_PROVIDER_ID = MODEL_PROVIDERS[0].id;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
428
469
|
// ../swarm-core/src/status.ts
|
|
429
470
|
function isAwaitingCi(status, stopReason) {
|
|
430
471
|
return status === "paused" && stopReason === AWAITING_CI_REASON;
|
|
@@ -477,7 +518,7 @@ var init_status = __esm({
|
|
|
477
518
|
// ../swarm-core/src/deps.ts
|
|
478
519
|
function judgeDep(runs) {
|
|
479
520
|
const real = runs.filter((r) => !r.service);
|
|
480
|
-
if (real.length === 0) return { ...
|
|
521
|
+
if (real.length === 0) return { ...UNKNOWN };
|
|
481
522
|
if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
|
|
482
523
|
if (real.some((r) => r.status === "done")) {
|
|
483
524
|
return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
|
|
@@ -497,12 +538,13 @@ function depClearedText(slug, v) {
|
|
|
497
538
|
const why = v.stopReason ? ` \u2014 ${v.stopReason}` : "";
|
|
498
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.`;
|
|
499
540
|
}
|
|
500
|
-
var PENDING, DEP_END_LABEL;
|
|
541
|
+
var PENDING, UNKNOWN, DEP_END_LABEL;
|
|
501
542
|
var init_deps = __esm({
|
|
502
543
|
"../swarm-core/src/deps.ts"() {
|
|
503
544
|
"use strict";
|
|
504
545
|
init_status();
|
|
505
546
|
PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
|
|
547
|
+
UNKNOWN = { outcome: "unknown", met: false, status: null, name: null, stopReason: null };
|
|
506
548
|
DEP_END_LABEL = {
|
|
507
549
|
stopped: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
|
|
508
550
|
failed: "\u0443\u043F\u0430\u043B",
|
|
@@ -521,6 +563,9 @@ function stampFromEpochSec(sec) {
|
|
|
521
563
|
function sqliteMs(ts) {
|
|
522
564
|
return Date.parse(ts.replace(" ", "T") + "Z");
|
|
523
565
|
}
|
|
566
|
+
function stampMs(at) {
|
|
567
|
+
return at.includes("T") ? Date.parse(at) : sqliteMs(at);
|
|
568
|
+
}
|
|
524
569
|
function toStamp(v) {
|
|
525
570
|
if (typeof v !== "string" || !v) return null;
|
|
526
571
|
const d = new Date(v);
|
|
@@ -989,6 +1034,42 @@ var init_cost = __esm({
|
|
|
989
1034
|
}
|
|
990
1035
|
});
|
|
991
1036
|
|
|
1037
|
+
// ../swarm-core/src/modelPricing.ts
|
|
1038
|
+
function findModelPricing(model, table = MODEL_PRICING) {
|
|
1039
|
+
if (!model) return void 0;
|
|
1040
|
+
const exact = table[model];
|
|
1041
|
+
if (exact) return exact;
|
|
1042
|
+
let bestKey = "";
|
|
1043
|
+
for (const key of Object.keys(table)) {
|
|
1044
|
+
if (model.startsWith(key) && key.length > bestKey.length) bestKey = key;
|
|
1045
|
+
}
|
|
1046
|
+
return bestKey ? table[bestKey] : void 0;
|
|
1047
|
+
}
|
|
1048
|
+
var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
|
|
1049
|
+
var init_modelPricing = __esm({
|
|
1050
|
+
"../swarm-core/src/modelPricing.ts"() {
|
|
1051
|
+
"use strict";
|
|
1052
|
+
MODEL_PRICING = {
|
|
1053
|
+
// Sonnet 5 — 2/10. Цена ПОСТОЯННАЯ: 18.08.2026 владелец подтвердил, что вводной
|
|
1054
|
+
// она быть перестала. Даты годности у тарифа тут больше нет и заводить её заново не
|
|
1055
|
+
// нужно: тариф со сроком означает, что в календарный день расчёт молча меняет цифру,
|
|
1056
|
+
// и «почему прогон вчера стоил иначе» приходится выяснять по коду, а не по таблице.
|
|
1057
|
+
// Меняется цена — меняется строка, одним видимым коммитом.
|
|
1058
|
+
"claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
|
|
1059
|
+
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1060
|
+
"claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
|
|
1061
|
+
// Прогоны и вызовы на 4.x остаются в истории — строки держим ради их стоимости.
|
|
1062
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1063
|
+
"claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1064
|
+
"claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1065
|
+
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
1066
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1067
|
+
};
|
|
1068
|
+
CACHE_READ_FACTOR = 0.1;
|
|
1069
|
+
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
|
|
992
1073
|
// ../swarm-core/src/runGuards.ts
|
|
993
1074
|
function remainingRunBudgetUsd(maxCostUsd, spentUsd) {
|
|
994
1075
|
if (!(maxCostUsd > 0)) return null;
|
|
@@ -1193,31 +1274,6 @@ function buildZoneModel(entries, opts = {}) {
|
|
|
1193
1274
|
const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
|
|
1194
1275
|
return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
|
|
1195
1276
|
}
|
|
1196
|
-
function zoneForFiles(model, files) {
|
|
1197
|
-
if (!hasZoneModel(model) || !files.length) return null;
|
|
1198
|
-
const spineSet = new Set(model.spine);
|
|
1199
|
-
const touchedSpine = files.filter((f) => spineSet.has(f));
|
|
1200
|
-
const votes = /* @__PURE__ */ new Map();
|
|
1201
|
-
for (const f of new Set(files)) {
|
|
1202
|
-
const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
|
|
1203
|
-
if (z2) bump(votes, z2);
|
|
1204
|
-
}
|
|
1205
|
-
const top = topKey(votes);
|
|
1206
|
-
if (!top) {
|
|
1207
|
-
return {
|
|
1208
|
-
zone: SPINE_ZONE,
|
|
1209
|
-
confidence: 1,
|
|
1210
|
-
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(", ")})`,
|
|
1211
|
-
spine: true
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
return {
|
|
1215
|
-
zone: top.key,
|
|
1216
|
-
confidence: top.share,
|
|
1217
|
-
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}`,
|
|
1218
|
-
spine: touchedSpine.length > 0
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
1277
|
function suggestZonesFromText(model, text, limit = 3) {
|
|
1222
1278
|
if (!hasZoneModel(model)) return [];
|
|
1223
1279
|
const terms = tokenize(text);
|
|
@@ -1448,6 +1504,7 @@ var init_src = __esm({
|
|
|
1448
1504
|
init_reviewPhase();
|
|
1449
1505
|
init_mergeGate();
|
|
1450
1506
|
init_verifyVerdict();
|
|
1507
|
+
init_modelPolicy();
|
|
1451
1508
|
init_status();
|
|
1452
1509
|
init_deps();
|
|
1453
1510
|
init_time();
|
|
@@ -1463,6 +1520,7 @@ var init_src = __esm({
|
|
|
1463
1520
|
init_hostCommands();
|
|
1464
1521
|
init_relief();
|
|
1465
1522
|
init_cost();
|
|
1523
|
+
init_modelPricing();
|
|
1466
1524
|
init_runGuards();
|
|
1467
1525
|
init_codeZones();
|
|
1468
1526
|
init_effort();
|
|
@@ -1597,18 +1655,12 @@ var init_swarmFiles = __esm({
|
|
|
1597
1655
|
});
|
|
1598
1656
|
|
|
1599
1657
|
// src/pricing.ts
|
|
1600
|
-
function
|
|
1601
|
-
const
|
|
1602
|
-
if (!intro) return pricing;
|
|
1603
|
-
const day = at.toISOString().slice(0, 10);
|
|
1604
|
-
return day <= intro.until ? { inputPerMTok: intro.inputPerMTok, outputPerMTok: intro.outputPerMTok } : pricing;
|
|
1605
|
-
}
|
|
1606
|
-
function costFromUsage(usage, model, table = MODEL_PRICING, at = /* @__PURE__ */ new Date()) {
|
|
1607
|
-
const entry = model ? table[model] : void 0;
|
|
1658
|
+
function costFromUsage(usage, model, table = MODEL_PRICING) {
|
|
1659
|
+
const entry = findModelPricing(model, table);
|
|
1608
1660
|
if (!entry) {
|
|
1609
1661
|
return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
|
|
1610
1662
|
}
|
|
1611
|
-
const pricing =
|
|
1663
|
+
const pricing = entry;
|
|
1612
1664
|
const input = usage.input_tokens ?? 0;
|
|
1613
1665
|
const output = usage.output_tokens ?? 0;
|
|
1614
1666
|
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
@@ -1616,24 +1668,10 @@ function costFromUsage(usage, model, table = MODEL_PRICING, at = /* @__PURE__ */
|
|
|
1616
1668
|
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
1617
1669
|
return { costUsd };
|
|
1618
1670
|
}
|
|
1619
|
-
var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
|
|
1620
1671
|
var init_pricing = __esm({
|
|
1621
1672
|
"src/pricing.ts"() {
|
|
1622
1673
|
"use strict";
|
|
1623
|
-
|
|
1624
|
-
// Sonnet 5: вводные 2/10 по 31.08.2026 включительно, дальше базовые 3/15.
|
|
1625
|
-
"claude-sonnet-5": {
|
|
1626
|
-
inputPerMTok: 3,
|
|
1627
|
-
outputPerMTok: 15,
|
|
1628
|
-
intro: { inputPerMTok: 2, outputPerMTok: 10, until: "2026-08-31" }
|
|
1629
|
-
},
|
|
1630
|
-
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1631
|
-
// Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
|
|
1632
|
-
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1633
|
-
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1634
|
-
};
|
|
1635
|
-
CACHE_READ_FACTOR = 0.1;
|
|
1636
|
-
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
1674
|
+
init_src();
|
|
1637
1675
|
}
|
|
1638
1676
|
});
|
|
1639
1677
|
|
|
@@ -2053,6 +2091,7 @@ __export(config_exports, {
|
|
|
2053
2091
|
REPO_MAP_PATH: () => REPO_MAP_PATH,
|
|
2054
2092
|
REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
|
|
2055
2093
|
RUN_LOGS_DIR: () => RUN_LOGS_DIR,
|
|
2094
|
+
SWARM_ANOMALY_DEP_MISSING_MS: () => SWARM_ANOMALY_DEP_MISSING_MS,
|
|
2056
2095
|
SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
2057
2096
|
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
2058
2097
|
SWARM_ANOMALY_MAX_REPEATS: () => SWARM_ANOMALY_MAX_REPEATS,
|
|
@@ -2095,6 +2134,7 @@ __export(config_exports, {
|
|
|
2095
2134
|
SWARM_REPO: () => SWARM_REPO,
|
|
2096
2135
|
SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
|
|
2097
2136
|
SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
|
|
2137
|
+
SWARM_REPO_SCAN_TTL_MS: () => SWARM_REPO_SCAN_TTL_MS,
|
|
2098
2138
|
SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
|
|
2099
2139
|
SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
|
|
2100
2140
|
SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
|
|
@@ -2116,6 +2156,7 @@ __export(config_exports, {
|
|
|
2116
2156
|
WORKTREES_LEGACY_DIR: () => WORKTREES_LEGACY_DIR,
|
|
2117
2157
|
WORKTREES_NOINDEX_DIR: () => WORKTREES_NOINDEX_DIR,
|
|
2118
2158
|
droneWorktreePath: () => droneWorktreePath,
|
|
2159
|
+
droneWorktreeRoot: () => droneWorktreeRoot,
|
|
2119
2160
|
mcpEndpoint: () => mcpEndpoint,
|
|
2120
2161
|
migrateWorktreesToNoindex: () => migrateWorktreesToNoindex,
|
|
2121
2162
|
normalizeRepo: () => normalizeRepo,
|
|
@@ -2134,7 +2175,10 @@ function normalizeRepo(repo) {
|
|
|
2134
2175
|
}
|
|
2135
2176
|
}
|
|
2136
2177
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
2137
|
-
return join5(worktreesDir,
|
|
2178
|
+
return join5(droneWorktreeRoot(worktreesDir, repo), slug);
|
|
2179
|
+
}
|
|
2180
|
+
function droneWorktreeRoot(worktreesDir, repo) {
|
|
2181
|
+
return join5(worktreesDir, basename(normalizeRepo(repo)));
|
|
2138
2182
|
}
|
|
2139
2183
|
function resolveEffectiveWorktreesDir(deps) {
|
|
2140
2184
|
if (deps.envOverride) return deps.envOverride;
|
|
@@ -2196,7 +2240,7 @@ function reviewCutoffAt() {
|
|
|
2196
2240
|
}
|
|
2197
2241
|
return at;
|
|
2198
2242
|
}
|
|
2199
|
-
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;
|
|
2200
2244
|
var init_config = __esm({
|
|
2201
2245
|
"src/config.ts"() {
|
|
2202
2246
|
"use strict";
|
|
@@ -2258,6 +2302,7 @@ var init_config = __esm({
|
|
|
2258
2302
|
SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
|
|
2259
2303
|
REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
|
|
2260
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);
|
|
2261
2306
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
2262
2307
|
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
2263
2308
|
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
@@ -2284,6 +2329,7 @@ var init_config = __esm({
|
|
|
2284
2329
|
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
2285
2330
|
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
2286
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);
|
|
2287
2333
|
SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
|
|
2288
2334
|
}
|
|
2289
2335
|
});
|
|
@@ -2682,11 +2728,11 @@ var init_storm = __esm({
|
|
|
2682
2728
|
});
|
|
2683
2729
|
|
|
2684
2730
|
// src/heartbeat.ts
|
|
2685
|
-
import { readFileSync as readFileSync5, renameSync as
|
|
2731
|
+
import { readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
2686
2732
|
function writeHeartbeat(path, now) {
|
|
2687
2733
|
const tmp = `${path}.tmp`;
|
|
2688
2734
|
writeFileSync5(tmp, String(now));
|
|
2689
|
-
|
|
2735
|
+
renameSync4(tmp, path);
|
|
2690
2736
|
}
|
|
2691
2737
|
function readHeartbeat(path) {
|
|
2692
2738
|
try {
|
|
@@ -2709,7 +2755,7 @@ var init_heartbeat = __esm({
|
|
|
2709
2755
|
|
|
2710
2756
|
// src/agentToken.ts
|
|
2711
2757
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
2712
|
-
import { join as
|
|
2758
|
+
import { join as join9 } from "path";
|
|
2713
2759
|
function loadAgentToken() {
|
|
2714
2760
|
try {
|
|
2715
2761
|
const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
|
|
@@ -2765,7 +2811,7 @@ var init_agentToken = __esm({
|
|
|
2765
2811
|
"src/agentToken.ts"() {
|
|
2766
2812
|
"use strict";
|
|
2767
2813
|
init_config();
|
|
2768
|
-
STORE_PATH =
|
|
2814
|
+
STORE_PATH = join9(SWARM_HOME, "agent.json");
|
|
2769
2815
|
base = (api) => api.replace(/\/$/, "");
|
|
2770
2816
|
TOKEN_PROBE_TIMEOUT_MS = 8e3;
|
|
2771
2817
|
}
|
|
@@ -3125,9 +3171,9 @@ var init_procCleanup = __esm({
|
|
|
3125
3171
|
});
|
|
3126
3172
|
|
|
3127
3173
|
// src/git.ts
|
|
3128
|
-
import { existsSync as
|
|
3174
|
+
import { existsSync as existsSync10, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
|
|
3129
3175
|
import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3130
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3176
|
+
import { isAbsolute as isAbsolute2, join as join17, sep as sep2 } from "path";
|
|
3131
3177
|
import { $ as $2 } from "execa";
|
|
3132
3178
|
function isUnrecoverableRefError(e) {
|
|
3133
3179
|
if (e instanceof MissingRefError) return true;
|
|
@@ -3139,7 +3185,7 @@ async function reclaimStaleWorktree(repo, worktree) {
|
|
|
3139
3185
|
}
|
|
3140
3186
|
function isLinkedWorktree(dir) {
|
|
3141
3187
|
try {
|
|
3142
|
-
return
|
|
3188
|
+
return statSync8(join17(dir, ".git")).isFile();
|
|
3143
3189
|
} catch {
|
|
3144
3190
|
return false;
|
|
3145
3191
|
}
|
|
@@ -3151,7 +3197,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
3151
3197
|
}
|
|
3152
3198
|
}
|
|
3153
3199
|
async function ensureWorktreeDirGone(worktree) {
|
|
3154
|
-
if (!
|
|
3200
|
+
if (!existsSync10(worktree)) return false;
|
|
3155
3201
|
if (!isLinkedWorktree(worktree)) {
|
|
3156
3202
|
console.error(
|
|
3157
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.`
|
|
@@ -3166,7 +3212,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
3166
3212
|
);
|
|
3167
3213
|
return true;
|
|
3168
3214
|
}
|
|
3169
|
-
if (
|
|
3215
|
+
if (existsSync10(worktree)) {
|
|
3170
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.`);
|
|
3171
3217
|
}
|
|
3172
3218
|
return true;
|
|
@@ -3214,7 +3260,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
3214
3260
|
}
|
|
3215
3261
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
3216
3262
|
if (busyAt) {
|
|
3217
|
-
const exists =
|
|
3263
|
+
const exists = existsSync10(busyAt);
|
|
3218
3264
|
const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
|
|
3219
3265
|
if (live) throw new WorktreeBusyError(branch, busyAt);
|
|
3220
3266
|
if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
|
|
@@ -3249,8 +3295,8 @@ async function ensureGitExcluded(worktree, pattern) {
|
|
|
3249
3295
|
try {
|
|
3250
3296
|
const commonDir = await gitPath(worktree, "--git-common-dir");
|
|
3251
3297
|
if (!commonDir) return false;
|
|
3252
|
-
const infoDir =
|
|
3253
|
-
const excludeFile =
|
|
3298
|
+
const infoDir = join17(commonDir, "info");
|
|
3299
|
+
const excludeFile = join17(infoDir, "exclude");
|
|
3254
3300
|
let current = "";
|
|
3255
3301
|
try {
|
|
3256
3302
|
current = await readFile(excludeFile, "utf8");
|
|
@@ -3270,7 +3316,7 @@ async function gitPath(worktree, flag) {
|
|
|
3270
3316
|
const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
|
|
3271
3317
|
const raw = out.stdout.trim();
|
|
3272
3318
|
if (out.exitCode !== 0 || !raw) return null;
|
|
3273
|
-
return isAbsolute2(raw) ? raw :
|
|
3319
|
+
return isAbsolute2(raw) ? raw : join17(worktree, raw);
|
|
3274
3320
|
}
|
|
3275
3321
|
async function ensureWorktreeHygiene(worktree) {
|
|
3276
3322
|
for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
|
|
@@ -3280,12 +3326,12 @@ async function installGuardHooks(worktree) {
|
|
|
3280
3326
|
try {
|
|
3281
3327
|
const gitDir = await gitPath(worktree, "--git-dir");
|
|
3282
3328
|
if (!gitDir) return false;
|
|
3283
|
-
const hooksDir =
|
|
3329
|
+
const hooksDir = join17(gitDir, "swarm-hooks");
|
|
3284
3330
|
const prev = await previousHooksPath(worktree);
|
|
3285
3331
|
await mkdir(hooksDir, { recursive: true });
|
|
3286
3332
|
for (const hook of GUARD_HOOKS) {
|
|
3287
|
-
await writeFile(
|
|
3288
|
-
await chmod(
|
|
3333
|
+
await writeFile(join17(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
|
|
3334
|
+
await chmod(join17(hooksDir, hook), 493);
|
|
3289
3335
|
}
|
|
3290
3336
|
await $2`git -C ${worktree} config extensions.worktreeConfig true`;
|
|
3291
3337
|
await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
|
|
@@ -3304,7 +3350,7 @@ async function previousHooksPath(worktree) {
|
|
|
3304
3350
|
if (out.exitCode === 0 && value) return value;
|
|
3305
3351
|
}
|
|
3306
3352
|
const commonDir = await gitPath(worktree, "--git-common-dir");
|
|
3307
|
-
return commonDir ?
|
|
3353
|
+
return commonDir ? join17(commonDir, "hooks") : null;
|
|
3308
3354
|
}
|
|
3309
3355
|
async function removeWorktree(repo, path) {
|
|
3310
3356
|
const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
@@ -3323,7 +3369,7 @@ async function worktreeDirty(path) {
|
|
|
3323
3369
|
async function repairMovedWorktrees(targetDir) {
|
|
3324
3370
|
const subdirs = (dir) => {
|
|
3325
3371
|
try {
|
|
3326
|
-
return
|
|
3372
|
+
return readdirSync11(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join17(dir, d.name));
|
|
3327
3373
|
} catch {
|
|
3328
3374
|
return [];
|
|
3329
3375
|
}
|
|
@@ -3450,7 +3496,7 @@ __export(droneChannel_exports, {
|
|
|
3450
3496
|
verifyLocalCliToken: () => verifyLocalCliToken
|
|
3451
3497
|
});
|
|
3452
3498
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
3453
|
-
import { chmodSync, mkdirSync as
|
|
3499
|
+
import { chmodSync, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
3454
3500
|
import { dirname as dirname3 } from "path";
|
|
3455
3501
|
import { serve } from "@hono/node-server";
|
|
3456
3502
|
function loadChannelSecret(path) {
|
|
@@ -3460,7 +3506,7 @@ function loadChannelSecret(path) {
|
|
|
3460
3506
|
} catch {
|
|
3461
3507
|
}
|
|
3462
3508
|
const secret = randomBytes(32).toString("hex");
|
|
3463
|
-
|
|
3509
|
+
mkdirSync8(dirname3(path), { recursive: true });
|
|
3464
3510
|
writeFileSync10(path, secret, { mode: 384 });
|
|
3465
3511
|
try {
|
|
3466
3512
|
chmodSync(path, 384);
|
|
@@ -3805,7 +3851,7 @@ var init_droneChannel = __esm({
|
|
|
3805
3851
|
|
|
3806
3852
|
// src/attachments.ts
|
|
3807
3853
|
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
3808
|
-
import { join as
|
|
3854
|
+
import { join as join20 } from "path";
|
|
3809
3855
|
function attachmentsFromDoc(doc, origin) {
|
|
3810
3856
|
const out = [];
|
|
3811
3857
|
const walk2 = (node) => {
|
|
@@ -3896,7 +3942,7 @@ async function materializeAttachments(opts) {
|
|
|
3896
3942
|
};
|
|
3897
3943
|
const result = { files: [], skipped: [] };
|
|
3898
3944
|
if (opts.refs.length === 0) return result;
|
|
3899
|
-
const dir =
|
|
3945
|
+
const dir = join20(opts.worktree, ATTACHMENTS_DIR);
|
|
3900
3946
|
await io.removeDir(dir);
|
|
3901
3947
|
let dirReady = false;
|
|
3902
3948
|
let total = 0;
|
|
@@ -3942,7 +3988,7 @@ async function materializeAttachments(opts) {
|
|
|
3942
3988
|
await io.mkdir(dir);
|
|
3943
3989
|
dirReady = true;
|
|
3944
3990
|
}
|
|
3945
|
-
await io.writeFile(
|
|
3991
|
+
await io.writeFile(join20(dir, name), file.bytes);
|
|
3946
3992
|
} catch (e) {
|
|
3947
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)})`);
|
|
3948
3994
|
continue;
|
|
@@ -4095,7 +4141,7 @@ var init_attachments = __esm({
|
|
|
4095
4141
|
|
|
4096
4142
|
// src/shotUpload.ts
|
|
4097
4143
|
import { readFile as fsReadFile } from "fs/promises";
|
|
4098
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
4144
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join21 } from "path";
|
|
4099
4145
|
function shotMimetype(path) {
|
|
4100
4146
|
const dot = path.lastIndexOf(".");
|
|
4101
4147
|
if (dot < 0) return null;
|
|
@@ -4121,7 +4167,7 @@ async function readShots(paths, opts) {
|
|
|
4121
4167
|
}
|
|
4122
4168
|
let bytes;
|
|
4123
4169
|
try {
|
|
4124
|
-
bytes = await read(isAbsolute3(path) ? path :
|
|
4170
|
+
bytes = await read(isAbsolute3(path) ? path : join21(opts.worktree, path));
|
|
4125
4171
|
} catch (e) {
|
|
4126
4172
|
skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
|
|
4127
4173
|
continue;
|
|
@@ -4200,7 +4246,7 @@ var init_shotUpload = __esm({
|
|
|
4200
4246
|
// src/oauth.ts
|
|
4201
4247
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
4202
4248
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
|
|
4203
|
-
import { join as
|
|
4249
|
+
import { join as join22 } from "path";
|
|
4204
4250
|
function makePkce() {
|
|
4205
4251
|
const verifier = randomBytes2(48).toString("base64url");
|
|
4206
4252
|
const challenge = createHash2("sha256").update(verifier).digest("base64url");
|
|
@@ -4335,8 +4381,8 @@ var init_oauth = __esm({
|
|
|
4335
4381
|
AUTHORIZE = "/api/oauth/authorize";
|
|
4336
4382
|
TOKEN = "/api/oauth/token";
|
|
4337
4383
|
SCOPE = "openid profile email taskless:full";
|
|
4338
|
-
STORE_PATH2 =
|
|
4339
|
-
CLIENT_PATH =
|
|
4384
|
+
STORE_PATH2 = join22(SWARM_HOME, "oauth.json");
|
|
4385
|
+
CLIENT_PATH = join22(SWARM_HOME, "oauth-client.json");
|
|
4340
4386
|
base2 = (api) => api.replace(/\/$/, "");
|
|
4341
4387
|
}
|
|
4342
4388
|
});
|
|
@@ -4361,6 +4407,10 @@ function authStateLine(nowMs = Date.now()) {
|
|
|
4361
4407
|
if (!denial) return null;
|
|
4362
4408
|
return `\u0414\u041E\u0421\u0422\u0423\u041F \u041A TASKLESS \u041E\u0422\u041E\u0417\u0412\u0410\u041D \u2014 ${deniedSinceLine(nowMs)}. \u041F\u043E\u0447\u0438\u043D\u0438 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: swarm login`;
|
|
4363
4409
|
}
|
|
4410
|
+
function signedInToTaskless() {
|
|
4411
|
+
if (denial) return false;
|
|
4412
|
+
return Boolean(loadAgentToken()?.token);
|
|
4413
|
+
}
|
|
4364
4414
|
function deniedSinceLine(nowMs) {
|
|
4365
4415
|
const d = denial;
|
|
4366
4416
|
if (!d) return "";
|
|
@@ -4379,7 +4429,7 @@ var init_authState = __esm({
|
|
|
4379
4429
|
|
|
4380
4430
|
// src/workspace.ts
|
|
4381
4431
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
|
|
4382
|
-
import { join as
|
|
4432
|
+
import { join as join23 } from "path";
|
|
4383
4433
|
function loadWorkspace() {
|
|
4384
4434
|
try {
|
|
4385
4435
|
const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
|
|
@@ -4396,7 +4446,7 @@ var init_workspace = __esm({
|
|
|
4396
4446
|
"src/workspace.ts"() {
|
|
4397
4447
|
"use strict";
|
|
4398
4448
|
init_config();
|
|
4399
|
-
WORKSPACE_PATH =
|
|
4449
|
+
WORKSPACE_PATH = join23(SWARM_HOME, "workspace.json");
|
|
4400
4450
|
}
|
|
4401
4451
|
});
|
|
4402
4452
|
|
|
@@ -4891,7 +4941,8 @@ __export(db_exports, {
|
|
|
4891
4941
|
SwarmDb: () => SwarmDb
|
|
4892
4942
|
});
|
|
4893
4943
|
import { createRequire as createRequire2 } from "module";
|
|
4894
|
-
import {
|
|
4944
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4945
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4895
4946
|
import { resolve as resolve3 } from "path";
|
|
4896
4947
|
function rowToQuestion(row) {
|
|
4897
4948
|
return {
|
|
@@ -4924,6 +4975,7 @@ function rowToRun(row) {
|
|
|
4924
4975
|
area: row.area ?? null,
|
|
4925
4976
|
depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
|
|
4926
4977
|
model: row.model ?? null,
|
|
4978
|
+
review_model: row.review_model ?? null,
|
|
4927
4979
|
// TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
|
|
4928
4980
|
// лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
|
|
4929
4981
|
// непонятное здесь означает «уровень не задан», а не сломанный тип.
|
|
@@ -5001,6 +5053,7 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
5001
5053
|
area TEXT,
|
|
5002
5054
|
depends_on TEXT,
|
|
5003
5055
|
model TEXT,
|
|
5056
|
+
review_model TEXT,
|
|
5004
5057
|
effort TEXT,
|
|
5005
5058
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
5006
5059
|
tokens_in INTEGER NOT NULL DEFAULT 0,
|
|
@@ -5051,6 +5104,12 @@ CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
|
|
|
5051
5104
|
CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
|
|
5052
5105
|
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
5053
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
|
+
);
|
|
5054
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)
|
|
5055
5114
|
CREATE TABLE IF NOT EXISTS imports (
|
|
5056
5115
|
source TEXT PRIMARY KEY,
|
|
@@ -5063,6 +5122,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5063
5122
|
defaultRepo;
|
|
5064
5123
|
databaseCtor;
|
|
5065
5124
|
// для importDb (второй Database, source)
|
|
5125
|
+
/** Идентификатор этой жизни файла базы (TASWM-419) — им неймспейсятся каталоги run-log'ов. */
|
|
5126
|
+
instanceId;
|
|
5066
5127
|
constructor(path = "swarm.db", defaultRepo = "") {
|
|
5067
5128
|
this.defaultRepo = normalizeRepo(defaultRepo);
|
|
5068
5129
|
this.databaseCtor = loadBetterSqlite3();
|
|
@@ -5114,7 +5175,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5114
5175
|
"ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
|
|
5115
5176
|
// TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
|
|
5116
5177
|
// NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
|
|
5117
|
-
"ALTER TABLE runs ADD COLUMN effort TEXT"
|
|
5178
|
+
"ALTER TABLE runs ADD COLUMN effort TEXT",
|
|
5179
|
+
// TASWM-451: чем проверять работу этого прогона — модель ревью, приехавшая с задачи.
|
|
5180
|
+
// Живёт на прогоне ИСПОЛНИТЕЛЯ: ревьюер и верификатор заводятся локально и читают её
|
|
5181
|
+
// оттуда, а не ходят за задачей в прод в момент спавна.
|
|
5182
|
+
"ALTER TABLE runs ADD COLUMN review_model TEXT"
|
|
5118
5183
|
]) {
|
|
5119
5184
|
try {
|
|
5120
5185
|
this.db.exec(alter);
|
|
@@ -5126,6 +5191,22 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5126
5191
|
if (this.defaultRepo) {
|
|
5127
5192
|
this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
|
|
5128
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;
|
|
5129
5210
|
}
|
|
5130
5211
|
close() {
|
|
5131
5212
|
this.db.close();
|
|
@@ -5134,7 +5215,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5134
5215
|
createRun(input) {
|
|
5135
5216
|
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
5136
5217
|
const res = this.db.prepare(
|
|
5137
|
-
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, effort, decision, resolves_run_id, reviews_run_id, verifies_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
5218
|
+
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, review_model, effort, decision, resolves_run_id, reviews_run_id, verifies_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
5138
5219
|
).run(
|
|
5139
5220
|
input.task_slug,
|
|
5140
5221
|
name,
|
|
@@ -5147,6 +5228,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5147
5228
|
input.area ?? null,
|
|
5148
5229
|
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
5149
5230
|
input.model ?? null,
|
|
5231
|
+
input.review_model ?? null,
|
|
5150
5232
|
input.effort ?? null,
|
|
5151
5233
|
// усилие рассуждения (TASWM-395); null → разрешит спавн
|
|
5152
5234
|
input.decision ? JSON.stringify(input.decision) : null,
|
|
@@ -5219,6 +5301,15 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5219
5301
|
const m = /^local:(\d+)$/.exec(key);
|
|
5220
5302
|
return m ? this.getRun(Number(m[1])) : null;
|
|
5221
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
|
+
}
|
|
5222
5313
|
listRuns(status) {
|
|
5223
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();
|
|
5224
5315
|
return rows.map(rowToRun);
|
|
@@ -5459,7 +5550,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5459
5550
|
const repoN = normalizeRepo(repo);
|
|
5460
5551
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
5461
5552
|
if (already) return { imported: false, runs: already.run_count };
|
|
5462
|
-
if (!
|
|
5553
|
+
if (!existsSync12(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
5463
5554
|
const src = new this.databaseCtor(abs);
|
|
5464
5555
|
try {
|
|
5465
5556
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
@@ -5828,8 +5919,8 @@ var init_droneStore = __esm({
|
|
|
5828
5919
|
|
|
5829
5920
|
// src/overmind.ts
|
|
5830
5921
|
import { homedir as homedir5 } from "os";
|
|
5831
|
-
import { existsSync as
|
|
5832
|
-
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";
|
|
5833
5924
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
5834
5925
|
import { spawn as spawn3 } from "child_process";
|
|
5835
5926
|
|
|
@@ -5909,13 +6000,66 @@ init_config();
|
|
|
5909
6000
|
init_resources();
|
|
5910
6001
|
init_names2();
|
|
5911
6002
|
init_storm();
|
|
5912
|
-
import { appendFileSync, existsSync as
|
|
6003
|
+
import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5913
6004
|
import { dirname } from "path";
|
|
5914
6005
|
import { randomUUID } from "crypto";
|
|
5915
6006
|
|
|
5916
6007
|
// src/logTail.ts
|
|
5917
|
-
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";
|
|
5918
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
|
|
5919
6063
|
var TAIL_LINES_FALLBACK = 200;
|
|
5920
6064
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
5921
6065
|
function fileHeader(name, size, shown) {
|
|
@@ -5925,7 +6069,7 @@ function fileHeader(name, size, shown) {
|
|
|
5925
6069
|
function readTail(path, opts) {
|
|
5926
6070
|
let size;
|
|
5927
6071
|
try {
|
|
5928
|
-
size =
|
|
6072
|
+
size = statSync4(path).size;
|
|
5929
6073
|
} catch {
|
|
5930
6074
|
return null;
|
|
5931
6075
|
}
|
|
@@ -5955,11 +6099,12 @@ function readTail(path, opts) {
|
|
|
5955
6099
|
text = rows.join("\n");
|
|
5956
6100
|
return { text, size, shown: Buffer.byteLength(text, "utf8") };
|
|
5957
6101
|
}
|
|
5958
|
-
function latestRunLaunch(runLogsDir,
|
|
5959
|
-
const dir =
|
|
6102
|
+
function latestRunLaunch(runLogsDir, runLogKeys) {
|
|
6103
|
+
const dir = findRunLogDir(runLogsDir, runLogKeys);
|
|
6104
|
+
if (!dir) return null;
|
|
5960
6105
|
let files;
|
|
5961
6106
|
try {
|
|
5962
|
-
files =
|
|
6107
|
+
files = readdirSync5(dir);
|
|
5963
6108
|
} catch {
|
|
5964
6109
|
return null;
|
|
5965
6110
|
}
|
|
@@ -5970,13 +6115,17 @@ function latestRunLaunch(runLogsDir, localRunId) {
|
|
|
5970
6115
|
}
|
|
5971
6116
|
if (indexes.size === 0) return null;
|
|
5972
6117
|
const index = Math.max(...indexes);
|
|
5973
|
-
const out = [
|
|
5974
|
-
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;
|
|
5975
6120
|
return { index, total: indexes.size, out, err };
|
|
5976
6121
|
}
|
|
5977
|
-
function runLogTail(runLogsDir,
|
|
5978
|
-
const launch = latestRunLaunch(runLogsDir,
|
|
5979
|
-
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
|
+
}
|
|
5980
6129
|
const parts = [];
|
|
5981
6130
|
let budget = opts.maxBytes;
|
|
5982
6131
|
if (launch.err) {
|
|
@@ -6031,8 +6180,8 @@ async function serveLogRequests(deps, commands) {
|
|
|
6031
6180
|
} else if (!cmd.runId) {
|
|
6032
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" };
|
|
6033
6182
|
} else {
|
|
6034
|
-
const
|
|
6035
|
-
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);
|
|
6036
6185
|
}
|
|
6037
6186
|
} catch (e) {
|
|
6038
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)}` };
|
|
@@ -6128,6 +6277,11 @@ var SwarmClient = class _SwarmClient {
|
|
|
6128
6277
|
// Живёт в памяти: рестарт демона просто переспросит прод.
|
|
6129
6278
|
depsProd = /* @__PURE__ */ new Map();
|
|
6130
6279
|
depsAskedAt = /* @__PURE__ */ new Map();
|
|
6280
|
+
// TASWM-399: ссылка dependsOn → человеко-слаг блокера, как его назвал прод. В колонке
|
|
6281
|
+
// лежит cuid прогона или задачи, а в лог демона, в событие прогона и в аномалию должен
|
|
6282
|
+
// уходить «TASWM-392», а не «cmsqqpphd011y2xtbckkut6bj» — иначе оператору всё равно
|
|
6283
|
+
// нечего искать. Резолв делает прод (см. resolveDepRefs), здесь только запоминаем.
|
|
6284
|
+
depSlug = /* @__PURE__ */ new Map();
|
|
6131
6285
|
static DEPS_TTL_MS = 6e4;
|
|
6132
6286
|
// TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
|
|
6133
6287
|
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
@@ -6187,25 +6341,25 @@ var SwarmClient = class _SwarmClient {
|
|
|
6187
6341
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
6188
6342
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
6189
6343
|
this.host = opts.host ?? null;
|
|
6190
|
-
this.repoExists = opts.repoExists ??
|
|
6344
|
+
this.repoExists = opts.repoExists ?? existsSync6;
|
|
6191
6345
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
6192
6346
|
this.executorId = opts.executorId ?? EXECUTOR.id;
|
|
6193
6347
|
this.outboxPath = opts.outboxPath;
|
|
6194
6348
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
6195
6349
|
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
6196
|
-
|
|
6350
|
+
mkdirSync5(dirname(this.outboxPath), { recursive: true });
|
|
6197
6351
|
this.recover();
|
|
6198
6352
|
}
|
|
6199
6353
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
6200
6354
|
recover() {
|
|
6201
|
-
if (
|
|
6355
|
+
if (existsSync6(this.mapPath)) {
|
|
6202
6356
|
try {
|
|
6203
6357
|
const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
|
|
6204
6358
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
6205
6359
|
} catch {
|
|
6206
6360
|
}
|
|
6207
6361
|
}
|
|
6208
|
-
if (
|
|
6362
|
+
if (existsSync6(this.outboxPath)) {
|
|
6209
6363
|
const txt = readFileSync4(this.outboxPath, "utf8");
|
|
6210
6364
|
for (const line of txt.split("\n")) {
|
|
6211
6365
|
const s = line.trim();
|
|
@@ -6226,7 +6380,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
6226
6380
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
6227
6381
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
6228
6382
|
replayGlobalEvents() {
|
|
6229
|
-
if (!
|
|
6383
|
+
if (!existsSync6(this.stormPath)) return;
|
|
6230
6384
|
let txt;
|
|
6231
6385
|
try {
|
|
6232
6386
|
txt = readFileSync4(this.stormPath, "utf8");
|
|
@@ -6306,6 +6460,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
6306
6460
|
if (patch.status === "running") out.executor = this.executorId;
|
|
6307
6461
|
if ("area" in patch) out.area = patch.area ?? null;
|
|
6308
6462
|
if ("model" in patch) out.model = patch.model ?? null;
|
|
6463
|
+
if ("review_model" in patch) out.reviewModel = patch.review_model ?? null;
|
|
6309
6464
|
if ("effort" in patch) out.effort = patch.effort ?? null;
|
|
6310
6465
|
if ("name" in patch) out.droneName = patch.name ?? null;
|
|
6311
6466
|
if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
|
|
@@ -6379,6 +6534,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
6379
6534
|
area: input.area ?? null,
|
|
6380
6535
|
depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
|
|
6381
6536
|
model: input.model ?? null,
|
|
6537
|
+
// TASWM-451: чем проверять эту работу. У прогона исполнителя приезжает из задачи
|
|
6538
|
+
// (claim в проде), у локально заведённого ревьюера/верификатора — наследством от
|
|
6539
|
+
// проверяемого прогона, которое ставит spawnReviewer/spawnVerifier.
|
|
6540
|
+
review_model: input.review_model ?? null,
|
|
6382
6541
|
// Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
|
|
6383
6542
|
// разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
|
|
6384
6543
|
effort: input.effort ?? null,
|
|
@@ -6437,6 +6596,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
6437
6596
|
status: "queued",
|
|
6438
6597
|
area: d.area,
|
|
6439
6598
|
model: d.model,
|
|
6599
|
+
review_model: d.review_model,
|
|
6600
|
+
// TASWM-451: чем проверять — едет рядом с моделью
|
|
6440
6601
|
effort: d.effort,
|
|
6441
6602
|
// усилие рассуждения (TASWM-395), если пинили на спавне
|
|
6442
6603
|
name: d.name,
|
|
@@ -6484,6 +6645,24 @@ var SwarmClient = class _SwarmClient {
|
|
|
6484
6645
|
const viaProd = [...this.runs.values()].find((r) => r.runKey === `p:${prodId}` || this.prodIdByKey.get(r.runKey) === prodId);
|
|
6485
6646
|
return viaProd ? this.mat(viaProd) : null;
|
|
6486
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
|
+
}
|
|
6487
6666
|
listRuns(status) {
|
|
6488
6667
|
const all = [...this.runs.values()].filter((r) => status ? r.status === status : true);
|
|
6489
6668
|
all.sort((a, b) => b.id - a.id);
|
|
@@ -6600,16 +6779,25 @@ var SwarmClient = class _SwarmClient {
|
|
|
6600
6779
|
/**
|
|
6601
6780
|
* TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
|
|
6602
6781
|
*
|
|
6603
|
-
*
|
|
6604
|
-
* (
|
|
6605
|
-
* стоят разного:
|
|
6606
|
-
* • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он
|
|
6607
|
-
* портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
|
|
6608
|
-
* •
|
|
6609
|
-
* на соседней машине как раз и может быть
|
|
6610
|
-
*
|
|
6611
|
-
*
|
|
6612
|
-
*
|
|
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
|
+
* незнанием такой вывод не делается.
|
|
6613
6801
|
*/
|
|
6614
6802
|
depVerdicts(depends_on) {
|
|
6615
6803
|
if (!depends_on || depends_on.length === 0) return [];
|
|
@@ -6619,57 +6807,89 @@ var SwarmClient = class _SwarmClient {
|
|
|
6619
6807
|
doneInMirror.add(r.task_slug);
|
|
6620
6808
|
}
|
|
6621
6809
|
}
|
|
6622
|
-
return depends_on.map((
|
|
6623
|
-
if (doneInMirror.has(
|
|
6624
|
-
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 };
|
|
6625
6820
|
}
|
|
6626
|
-
|
|
6627
|
-
if (fromProd) return { slug, ...fromProd };
|
|
6628
|
-
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 };
|
|
6629
6822
|
});
|
|
6630
6823
|
}
|
|
6631
6824
|
/**
|
|
6632
|
-
*
|
|
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
|
+
* прогон в очереди вечно.
|
|
6633
6846
|
*
|
|
6634
|
-
*
|
|
6635
|
-
*
|
|
6636
|
-
*
|
|
6637
|
-
*
|
|
6638
|
-
* (
|
|
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` на прод-стороне).
|
|
6639
6853
|
*
|
|
6640
6854
|
* Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
|
|
6641
6855
|
* положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
|
|
6642
|
-
* «исчерпана
|
|
6643
|
-
*
|
|
6644
|
-
*
|
|
6856
|
+
* «исчерпана неуспешно» и «блокера не знает никто») переспрашиваем не чаще DEPS_TTL_MS:
|
|
6857
|
+
* оператор вправе переиграть блокер новым прогоном, а тик идёт раз в ~4 с — без TTL это
|
|
6858
|
+
* был бы поход в прод на каждую ссылку каждый тик.
|
|
6859
|
+
*
|
|
6860
|
+
* Одним вызовом на все просроченные ссылки, а не по вызову на ссылку: очередь ждущих
|
|
6861
|
+
* бывает в десятки прогонов, и N запросов на тик — это тот же самый поход в прод, только
|
|
6862
|
+
* дороже.
|
|
6645
6863
|
*/
|
|
6646
|
-
async refreshDeps(
|
|
6647
|
-
|
|
6648
|
-
if (!
|
|
6649
|
-
const asked = this.depsAskedAt.get(
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
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);
|
|
6673
6893
|
}
|
|
6674
6894
|
}
|
|
6675
6895
|
/**
|
|
@@ -6988,7 +7208,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
6988
7208
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
6989
7209
|
*/
|
|
6990
7210
|
reconcileRuntime(opts) {
|
|
6991
|
-
const wtExists = opts.worktreeExists ??
|
|
7211
|
+
const wtExists = opts.worktreeExists ?? existsSync6;
|
|
6992
7212
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
6993
7213
|
try {
|
|
6994
7214
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -7477,6 +7697,12 @@ var SwarmClient = class _SwarmClient {
|
|
|
7477
7697
|
...typeof h.paused === "boolean" ? { paused: h.paused } : {},
|
|
7478
7698
|
...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
|
|
7479
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 } : {},
|
|
7480
7706
|
// Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
|
|
7481
7707
|
// ровно как с паузой выше: «не знаем» и так значит «не знаем».
|
|
7482
7708
|
...resources ? { resources } : {}
|
|
@@ -7562,9 +7788,18 @@ var SwarmClient = class _SwarmClient {
|
|
|
7562
7788
|
* ВСЕГДА (в т.ч. `storm:false`), как паузу: это состояние, а не команда, и молчание тут
|
|
7563
7789
|
* означало бы «прод помнит вчерашний шторм», когда рой давно работает.
|
|
7564
7790
|
*
|
|
7791
|
+
* TASWM-402: тем же пульсом уезжают КОРНИ WORKTREE'ЕВ этой машины (`worktreeRoots` —
|
|
7792
|
+
* `owner/repo` → `<worktreesDir>/<repo-basename>`). Это НЕ рантайм и правило «pid и путь
|
|
7793
|
+
* дрона в прод не уходят» не трогает: корень — статичная конфигурация машины, того же
|
|
7794
|
+
* рода, что реестр репо и ёмкость, он не зависит ни от прогона, ни от живости процесса и
|
|
7795
|
+
* не протухает между пульсами. Прод склеивает `<корень>/<слаг>` сам — и отвечает человеку
|
|
7796
|
+
* на вопрос «куда идти руками», когда команда машине не доиграла (а в этом случае, по
|
|
7797
|
+
* определению, спросить рантайм у молчащей машины уже нельзя). Не передали корень (не
|
|
7798
|
+
* знаем своего каталога) → ключа нет, прод прошлое не трогает — как с capacity/resources.
|
|
7799
|
+
*
|
|
7565
7800
|
* Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
|
|
7566
7801
|
*/
|
|
7567
|
-
async heartbeat(daemonVersion, pause, capacity, resources, storm) {
|
|
7802
|
+
async heartbeat(daemonVersion, pause, capacity, resources, storm, worktreesDir, signIn) {
|
|
7568
7803
|
try {
|
|
7569
7804
|
const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
|
|
7570
7805
|
if (daemonVersion) args.daemonVersion = daemonVersion;
|
|
@@ -7574,10 +7809,19 @@ var SwarmClient = class _SwarmClient {
|
|
|
7574
7809
|
args.paused = !!pause?.paused;
|
|
7575
7810
|
if (pause?.reason) args.pauseReason = pause.reason;
|
|
7576
7811
|
}
|
|
7812
|
+
if (signIn) {
|
|
7813
|
+
args.signedIn = signIn.signedIn;
|
|
7814
|
+
if (signIn.webUrl) args.webUrl = signIn.webUrl;
|
|
7815
|
+
}
|
|
7577
7816
|
if (storm !== void 0 && storm !== null) {
|
|
7578
7817
|
args.storm = storm.active;
|
|
7579
7818
|
if (storm.reason) args.stormReason = storm.reason;
|
|
7580
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
|
+
}
|
|
7581
7825
|
if (this.host) {
|
|
7582
7826
|
args.hostId = this.host.hostId;
|
|
7583
7827
|
args.hostName = this.host.hostName;
|
|
@@ -7726,10 +7970,20 @@ var SwarmClient = class _SwarmClient {
|
|
|
7726
7970
|
* именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
|
|
7727
7971
|
* repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
|
|
7728
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, мы сломали бы его.
|
|
7729
7983
|
*/
|
|
7730
7984
|
resolveRepo(raw) {
|
|
7731
7985
|
if (!raw) return "";
|
|
7732
|
-
return this.repoPathByFullName.get(raw) ?? raw;
|
|
7986
|
+
return this.repoPathByFullName.get(raw) ?? this.repoPathByFullName.get(raw.toLowerCase()) ?? raw;
|
|
7733
7987
|
}
|
|
7734
7988
|
/**
|
|
7735
7989
|
* TASWM-281: МОЯ ли это работа. Три исхода:
|
|
@@ -7805,6 +8059,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
7805
8059
|
// существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
|
|
7806
8060
|
// в отличие от respawns, — авто-ревью v2, TASWM-162.
|
|
7807
8061
|
review_cycle: 0,
|
|
8062
|
+
// TASWM-451: модель ПРОВЕРКИ — прод-поле, заполняется ниже из ответа (как base_branch).
|
|
8063
|
+
review_model: null,
|
|
7808
8064
|
task_title: null,
|
|
7809
8065
|
task_desc: null,
|
|
7810
8066
|
task_url: null,
|
|
@@ -7857,6 +8113,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7857
8113
|
local.area = pr.area ?? null;
|
|
7858
8114
|
local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
|
|
7859
8115
|
local.model = pr.model ?? null;
|
|
8116
|
+
local.review_model = pr.reviewModel ?? null;
|
|
7860
8117
|
local.effort = toEffortLevel(pr.effort);
|
|
7861
8118
|
local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
|
|
7862
8119
|
local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
|
|
@@ -8167,20 +8424,20 @@ init_config();
|
|
|
8167
8424
|
init_heartbeat();
|
|
8168
8425
|
|
|
8169
8426
|
// src/logRotate.ts
|
|
8170
|
-
import { copyFileSync, renameSync as
|
|
8427
|
+
import { copyFileSync, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
|
|
8171
8428
|
var realIo = {
|
|
8172
8429
|
size: (path) => {
|
|
8173
8430
|
try {
|
|
8174
|
-
return
|
|
8431
|
+
return statSync5(path).size;
|
|
8175
8432
|
} catch {
|
|
8176
8433
|
return null;
|
|
8177
8434
|
}
|
|
8178
8435
|
},
|
|
8179
8436
|
copy: copyFileSync,
|
|
8180
|
-
rename:
|
|
8437
|
+
rename: renameSync5,
|
|
8181
8438
|
truncate: (path) => truncateSync(path, 0),
|
|
8182
8439
|
unlink: unlinkSync2,
|
|
8183
|
-
exists:
|
|
8440
|
+
exists: existsSync7
|
|
8184
8441
|
};
|
|
8185
8442
|
var realOwnerIo = {
|
|
8186
8443
|
statFd: (fd) => {
|
|
@@ -8193,7 +8450,7 @@ var realOwnerIo = {
|
|
|
8193
8450
|
},
|
|
8194
8451
|
statPath: (path) => {
|
|
8195
8452
|
try {
|
|
8196
|
-
const s =
|
|
8453
|
+
const s = statSync5(path);
|
|
8197
8454
|
return { dev: s.dev, ino: s.ino };
|
|
8198
8455
|
} catch {
|
|
8199
8456
|
return null;
|
|
@@ -8265,12 +8522,12 @@ async function buildRepoRegistry(paths) {
|
|
|
8265
8522
|
|
|
8266
8523
|
// src/repoMemory.ts
|
|
8267
8524
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
8268
|
-
import { join as
|
|
8525
|
+
import { join as join8 } from "path";
|
|
8269
8526
|
var FILE = "repos.json";
|
|
8270
8527
|
function loadRememberedRepos(home) {
|
|
8271
8528
|
let raw;
|
|
8272
8529
|
try {
|
|
8273
|
-
raw = readFileSync6(
|
|
8530
|
+
raw = readFileSync6(join8(home, FILE), "utf8");
|
|
8274
8531
|
} catch {
|
|
8275
8532
|
return [];
|
|
8276
8533
|
}
|
|
@@ -8285,7 +8542,7 @@ function loadRememberedRepos(home) {
|
|
|
8285
8542
|
function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
|
|
8286
8543
|
const clean = paths.filter((p) => p.trim().length > 0);
|
|
8287
8544
|
if (clean.length === 0) return;
|
|
8288
|
-
writeFileSync6(
|
|
8545
|
+
writeFileSync6(join8(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
|
|
8289
8546
|
`);
|
|
8290
8547
|
}
|
|
8291
8548
|
function pickRepoPaths(args) {
|
|
@@ -8300,12 +8557,12 @@ init_src();
|
|
|
8300
8557
|
init_config();
|
|
8301
8558
|
init_linkFailure();
|
|
8302
8559
|
import { homedir as homedir3 } from "os";
|
|
8303
|
-
import { readFileSync as readFileSync9, renameSync as
|
|
8304
|
-
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";
|
|
8305
8562
|
|
|
8306
8563
|
// src/repoScan.ts
|
|
8307
|
-
import { readdirSync as
|
|
8308
|
-
import { join as
|
|
8564
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
|
|
8565
|
+
import { join as join10 } from "path";
|
|
8309
8566
|
var SKIP = /* @__PURE__ */ new Set([
|
|
8310
8567
|
"node_modules",
|
|
8311
8568
|
"vendor",
|
|
@@ -8335,7 +8592,7 @@ function parseOriginFromGitConfig(text) {
|
|
|
8335
8592
|
}
|
|
8336
8593
|
function originOf(dir) {
|
|
8337
8594
|
try {
|
|
8338
|
-
return parseOriginFromGitConfig(readFileSync8(
|
|
8595
|
+
return parseOriginFromGitConfig(readFileSync8(join10(dir, ".git", "config"), "utf8"));
|
|
8339
8596
|
} catch {
|
|
8340
8597
|
return null;
|
|
8341
8598
|
}
|
|
@@ -8364,15 +8621,15 @@ function walk(dir, depth, maxDepth, found) {
|
|
|
8364
8621
|
}
|
|
8365
8622
|
let entries;
|
|
8366
8623
|
try {
|
|
8367
|
-
entries =
|
|
8624
|
+
entries = readdirSync6(dir);
|
|
8368
8625
|
} catch {
|
|
8369
8626
|
return;
|
|
8370
8627
|
}
|
|
8371
8628
|
for (const name of entries) {
|
|
8372
8629
|
if (name.startsWith(".") || SKIP.has(name)) continue;
|
|
8373
|
-
const child =
|
|
8630
|
+
const child = join10(dir, name);
|
|
8374
8631
|
try {
|
|
8375
|
-
if (!
|
|
8632
|
+
if (!statSync6(child).isDirectory()) continue;
|
|
8376
8633
|
} catch {
|
|
8377
8634
|
continue;
|
|
8378
8635
|
}
|
|
@@ -8392,7 +8649,7 @@ function defaultScanRoots(home, knownPaths = []) {
|
|
|
8392
8649
|
if (parent && parent !== p) add(parent);
|
|
8393
8650
|
}
|
|
8394
8651
|
for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
|
|
8395
|
-
add(
|
|
8652
|
+
add(join10(home, name));
|
|
8396
8653
|
}
|
|
8397
8654
|
return out;
|
|
8398
8655
|
}
|
|
@@ -8406,7 +8663,7 @@ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
|
|
|
8406
8663
|
for (const part of v.split(",")) {
|
|
8407
8664
|
const p = part.trim();
|
|
8408
8665
|
if (!p) continue;
|
|
8409
|
-
const abs = p === "~" ? home : p.startsWith("~/") ?
|
|
8666
|
+
const abs = p === "~" ? home : p.startsWith("~/") ? join11(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
|
|
8410
8667
|
if (!out.includes(abs)) out.push(abs);
|
|
8411
8668
|
}
|
|
8412
8669
|
return out;
|
|
@@ -8471,6 +8728,28 @@ function describeReposFailure(kind, detail) {
|
|
|
8471
8728
|
function emptyRepoMapState(roots) {
|
|
8472
8729
|
return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
|
|
8473
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
|
+
}
|
|
8474
8753
|
async function refreshRepoMap(opts) {
|
|
8475
8754
|
const log = opts.log ?? (() => {
|
|
8476
8755
|
});
|
|
@@ -8484,14 +8763,21 @@ async function refreshRepoMap(opts) {
|
|
|
8484
8763
|
clones.set(repoFullName, { repoFullName, path, source, alternatives });
|
|
8485
8764
|
};
|
|
8486
8765
|
for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
|
|
8487
|
-
|
|
8766
|
+
const cache = loadRepoMapCache(opts.cachePath);
|
|
8767
|
+
for (const e of cache.entries) {
|
|
8488
8768
|
const name = e.repoFullName.toLowerCase();
|
|
8489
8769
|
if (clones.has(name)) continue;
|
|
8490
8770
|
if (repoAt(e.path) === name) take(name, e.path, "cache");
|
|
8491
8771
|
}
|
|
8492
8772
|
const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
|
|
8493
|
-
const
|
|
8494
|
-
|
|
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) {
|
|
8495
8781
|
const startedAt = Date.now();
|
|
8496
8782
|
const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
|
|
8497
8783
|
let added = 0;
|
|
@@ -8500,9 +8786,10 @@ async function refreshRepoMap(opts) {
|
|
|
8500
8786
|
take(full, paths[0], "scan", paths.slice(1));
|
|
8501
8787
|
added++;
|
|
8502
8788
|
}
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
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
|
+
);
|
|
8506
8793
|
}
|
|
8507
8794
|
const registry = [...clones.values()].map((c) => ({
|
|
8508
8795
|
repoFullName: c.repoFullName,
|
|
@@ -8510,10 +8797,12 @@ async function refreshRepoMap(opts) {
|
|
|
8510
8797
|
}));
|
|
8511
8798
|
writeRepoMapCache(
|
|
8512
8799
|
opts.cachePath,
|
|
8513
|
-
[...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
|
|
8800
|
+
[...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path })),
|
|
8801
|
+
scannedAt
|
|
8514
8802
|
);
|
|
8515
8803
|
if (!answer.ok) {
|
|
8516
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)`);
|
|
8517
8806
|
return registry;
|
|
8518
8807
|
}
|
|
8519
8808
|
opts.state.unknown = false;
|
|
@@ -8525,7 +8814,7 @@ async function refreshRepoMap(opts) {
|
|
|
8525
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");
|
|
8526
8815
|
return registry;
|
|
8527
8816
|
}
|
|
8528
|
-
log(`[overmind]
|
|
8817
|
+
log(`[overmind] ${describeMapSummary(opts.state, opts.roots)}`);
|
|
8529
8818
|
for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
|
|
8530
8819
|
const prefixOf = new Map(answer.repos.map((r) => [r.repoFullName, r.taskPrefix]));
|
|
8531
8820
|
for (const name of opts.state.missing) {
|
|
@@ -8534,6 +8823,11 @@ async function refreshRepoMap(opts) {
|
|
|
8534
8823
|
}
|
|
8535
8824
|
return registry;
|
|
8536
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
|
+
}
|
|
8537
8831
|
function describeClone(c) {
|
|
8538
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";
|
|
8539
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` : "";
|
|
@@ -8554,40 +8848,43 @@ function repoMapRows(state) {
|
|
|
8554
8848
|
...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
|
|
8555
8849
|
];
|
|
8556
8850
|
}
|
|
8557
|
-
function
|
|
8851
|
+
function loadRepoMapCache(path) {
|
|
8558
8852
|
try {
|
|
8559
8853
|
const raw = JSON.parse(readFileSync9(path, "utf8"));
|
|
8560
|
-
if (!Array.isArray(raw?.entries)) return [];
|
|
8561
|
-
return
|
|
8562
|
-
|
|
8563
|
-
|
|
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
|
+
};
|
|
8564
8861
|
} catch {
|
|
8565
|
-
return [];
|
|
8862
|
+
return { scannedAt: null, entries: [] };
|
|
8566
8863
|
}
|
|
8567
8864
|
}
|
|
8568
|
-
function writeRepoMapCache(path, entries) {
|
|
8865
|
+
function writeRepoMapCache(path, entries, scannedAt = stamp()) {
|
|
8569
8866
|
try {
|
|
8570
8867
|
const tmp = `${path}.tmp`;
|
|
8571
|
-
writeFileSync8(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
|
|
8572
|
-
|
|
8868
|
+
writeFileSync8(tmp, JSON.stringify({ at: stamp(), scannedAt, entries }, null, 2));
|
|
8869
|
+
renameSync6(tmp, path);
|
|
8573
8870
|
} catch {
|
|
8574
8871
|
}
|
|
8575
8872
|
}
|
|
8576
8873
|
|
|
8577
8874
|
// src/hostIdentity.ts
|
|
8578
8875
|
import { hostname } from "os";
|
|
8579
|
-
import { existsSync as
|
|
8580
|
-
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";
|
|
8581
8878
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8582
8879
|
function hostIdentityPath(home) {
|
|
8583
|
-
return
|
|
8880
|
+
return join12(home, "host.json");
|
|
8584
8881
|
}
|
|
8585
8882
|
function loadHostIdentity(deps) {
|
|
8586
|
-
const exists = deps.exists ??
|
|
8883
|
+
const exists = deps.exists ?? existsSync8;
|
|
8587
8884
|
const readFile2 = deps.readFile ?? ((p) => readFileSync10(p, "utf8"));
|
|
8588
8885
|
const writeFile3 = deps.writeFile ?? ((p, data) => {
|
|
8589
8886
|
writeFileSync9(`${p}.tmp`, data);
|
|
8590
|
-
|
|
8887
|
+
renameSync7(`${p}.tmp`, p);
|
|
8591
8888
|
});
|
|
8592
8889
|
const host = deps.hostname ?? hostname;
|
|
8593
8890
|
const newId = deps.newId ?? randomUUID2;
|
|
@@ -8769,15 +9066,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
|
8769
9066
|
|
|
8770
9067
|
// src/launcher.ts
|
|
8771
9068
|
import { spawn } from "child_process";
|
|
8772
|
-
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";
|
|
8773
9070
|
import { createRequire } from "module";
|
|
8774
9071
|
import { homedir as homedir4 } from "os";
|
|
8775
|
-
import { dirname as dirname2, join as
|
|
9072
|
+
import { dirname as dirname2, join as join14 } from "path";
|
|
8776
9073
|
import { fileURLToPath } from "url";
|
|
8777
9074
|
|
|
8778
9075
|
// src/droneHome.ts
|
|
8779
|
-
import { lstatSync, mkdirSync as
|
|
8780
|
-
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";
|
|
8781
9078
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
8782
9079
|
function droneHomeDeny(env = process.env) {
|
|
8783
9080
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -8795,29 +9092,29 @@ function buildDroneHome(opts) {
|
|
|
8795
9092
|
const { realHome, droneHome } = opts;
|
|
8796
9093
|
const deny = opts.deny ?? droneHomeDeny();
|
|
8797
9094
|
const platform = opts.platform ?? process.platform;
|
|
8798
|
-
|
|
8799
|
-
const wanted = new Set(mirroredEntries(
|
|
9095
|
+
mkdirSync6(droneHome, { recursive: true, mode: 448 });
|
|
9096
|
+
const wanted = new Set(mirroredEntries(readdirSync7(realHome), deny, platform));
|
|
8800
9097
|
const rel = relative(realHome, droneHome);
|
|
8801
9098
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
8802
|
-
for (const existing of
|
|
9099
|
+
for (const existing of readdirSync7(droneHome)) {
|
|
8803
9100
|
if (wanted.has(existing)) continue;
|
|
8804
|
-
const p =
|
|
9101
|
+
const p = join13(droneHome, existing);
|
|
8805
9102
|
try {
|
|
8806
9103
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
8807
9104
|
} catch {
|
|
8808
9105
|
}
|
|
8809
9106
|
}
|
|
8810
9107
|
for (const name of wanted) {
|
|
8811
|
-
const link =
|
|
9108
|
+
const link = join13(droneHome, name);
|
|
8812
9109
|
try {
|
|
8813
9110
|
lstatSync(link);
|
|
8814
9111
|
continue;
|
|
8815
9112
|
} catch {
|
|
8816
9113
|
}
|
|
8817
|
-
const target =
|
|
9114
|
+
const target = join13(realHome, name);
|
|
8818
9115
|
let isDir;
|
|
8819
9116
|
try {
|
|
8820
|
-
isDir =
|
|
9117
|
+
isDir = statSync7(target).isDirectory();
|
|
8821
9118
|
} catch {
|
|
8822
9119
|
continue;
|
|
8823
9120
|
}
|
|
@@ -8835,7 +9132,7 @@ var REAL_PATH_ENV_PINS = [
|
|
|
8835
9132
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
8836
9133
|
];
|
|
8837
9134
|
function realPathEnvPins(realHome) {
|
|
8838
|
-
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)]));
|
|
8839
9136
|
}
|
|
8840
9137
|
function dronePrivatePaths(realHome, swarmHome) {
|
|
8841
9138
|
return [
|
|
@@ -8895,12 +9192,26 @@ function pick(v) {
|
|
|
8895
9192
|
const s = typeof v === "string" ? v.trim() : "";
|
|
8896
9193
|
return s.length > 0 ? s : null;
|
|
8897
9194
|
}
|
|
9195
|
+
function isReviewKind(kind) {
|
|
9196
|
+
return kind === "reviewer" || kind === "verifier";
|
|
9197
|
+
}
|
|
9198
|
+
function modelRoleOf(kind) {
|
|
9199
|
+
return isReviewKind(kind) ? "review" : "executor";
|
|
9200
|
+
}
|
|
8898
9201
|
function forKind(kind, swarm, norm) {
|
|
8899
9202
|
if (!swarm) return null;
|
|
8900
|
-
return kind
|
|
9203
|
+
return isReviewKind(kind) ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
|
|
9204
|
+
}
|
|
9205
|
+
function resolveRunModelRef(run, swarm, env = process.env) {
|
|
9206
|
+
const configured = parseModelRef(
|
|
9207
|
+
pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL)
|
|
9208
|
+
);
|
|
9209
|
+
if (configured) return configured;
|
|
9210
|
+
const providerId = pick(env.SWARM_MODEL_PROVIDER) ?? DEFAULT_PROVIDER_ID;
|
|
9211
|
+
return { providerId, model: defaultModelFor(modelRoleOf(run.kind), providerId) };
|
|
8901
9212
|
}
|
|
8902
9213
|
function resolveRunModel(run, swarm, env = process.env) {
|
|
8903
|
-
return
|
|
9214
|
+
return resolveRunModelRef(run, swarm, env).model;
|
|
8904
9215
|
}
|
|
8905
9216
|
function resolveRunEffort(run, swarm, env = process.env) {
|
|
8906
9217
|
return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
|
|
@@ -8933,7 +9244,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
|
8933
9244
|
const env = { ...base3 };
|
|
8934
9245
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
8935
9246
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
8936
|
-
env.TURBO_CACHE_DIR =
|
|
9247
|
+
env.TURBO_CACHE_DIR = join14(worktree, ".turbo", "cache");
|
|
8937
9248
|
}
|
|
8938
9249
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
8939
9250
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -8961,20 +9272,21 @@ var DAEMON_STATE_ENV = [
|
|
|
8961
9272
|
"TASKLESS_TOKEN"
|
|
8962
9273
|
];
|
|
8963
9274
|
function bundledRoleDir(kind) {
|
|
8964
|
-
return
|
|
9275
|
+
return join14(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
|
|
8965
9276
|
}
|
|
8966
9277
|
function roleDir(worktree, kind) {
|
|
8967
|
-
return
|
|
9278
|
+
return join14(worktree, ".claude", "roles", kind);
|
|
8968
9279
|
}
|
|
8969
9280
|
function roleDirs(worktree, kind) {
|
|
8970
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
9281
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync9(dir));
|
|
8971
9282
|
}
|
|
8972
|
-
function nextRunLogPath(runLogsDir,
|
|
8973
|
-
const dir =
|
|
8974
|
-
|
|
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 });
|
|
8975
9287
|
let files = [];
|
|
8976
9288
|
try {
|
|
8977
|
-
files =
|
|
9289
|
+
files = readdirSync8(dir);
|
|
8978
9290
|
} catch {
|
|
8979
9291
|
}
|
|
8980
9292
|
let next = 0;
|
|
@@ -8982,11 +9294,11 @@ function nextRunLogPath(runLogsDir, runId) {
|
|
|
8982
9294
|
const m = /^(\d+)\.json/.exec(f);
|
|
8983
9295
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
8984
9296
|
}
|
|
8985
|
-
return
|
|
9297
|
+
return join14(dir, `${next}.json`);
|
|
8986
9298
|
}
|
|
8987
|
-
function openNextRunLog(runLogsDir,
|
|
9299
|
+
function openNextRunLog(runLogsDir, runLogKeys) {
|
|
8988
9300
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
8989
|
-
const path = nextRunLogPath(runLogsDir,
|
|
9301
|
+
const path = nextRunLogPath(runLogsDir, runLogKeys);
|
|
8990
9302
|
let out;
|
|
8991
9303
|
try {
|
|
8992
9304
|
out = openSync2(path, "wx");
|
|
@@ -9002,7 +9314,7 @@ function openNextRunLog(runLogsDir, runId) {
|
|
|
9002
9314
|
}
|
|
9003
9315
|
return { out, err };
|
|
9004
9316
|
}
|
|
9005
|
-
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?`);
|
|
9006
9318
|
}
|
|
9007
9319
|
function makeLauncher(cfg) {
|
|
9008
9320
|
return ({ worktree, run, prompt, continueSession }) => {
|
|
@@ -9065,9 +9377,9 @@ function makeLauncher(cfg) {
|
|
|
9065
9377
|
}
|
|
9066
9378
|
const realHome = cfg.droneIsolation?.realHome ?? homedir4();
|
|
9067
9379
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
9068
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !
|
|
9380
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync9(join14(cfg.droneIsolation.swarmHome, "token"))) {
|
|
9069
9381
|
console.error(
|
|
9070
|
-
`[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.`
|
|
9071
9383
|
);
|
|
9072
9384
|
}
|
|
9073
9385
|
const executor = cfg.executor ?? resolveExecutor();
|
|
@@ -9094,7 +9406,13 @@ function makeLauncher(cfg) {
|
|
|
9094
9406
|
let outFd;
|
|
9095
9407
|
let errFd;
|
|
9096
9408
|
if (cfg.runLogsDir) {
|
|
9097
|
-
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);
|
|
9098
9416
|
outFd = fds.out;
|
|
9099
9417
|
errFd = fds.err ?? void 0;
|
|
9100
9418
|
stdio = ["ignore", outFd, errFd ?? "ignore"];
|
|
@@ -9122,21 +9440,21 @@ function makeLauncher(cfg) {
|
|
|
9122
9440
|
init_procCleanup();
|
|
9123
9441
|
|
|
9124
9442
|
// src/cost.ts
|
|
9443
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync9, renameSync as renameSync8 } from "fs";
|
|
9444
|
+
import { join as join15 } from "path";
|
|
9125
9445
|
init_executors();
|
|
9126
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
|
|
9127
|
-
import { join as join14 } from "path";
|
|
9128
9446
|
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
9129
9447
|
function unprocessedLogFiles(dir) {
|
|
9130
9448
|
try {
|
|
9131
|
-
return
|
|
9449
|
+
return readdirSync9(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
9132
9450
|
} catch {
|
|
9133
9451
|
return [];
|
|
9134
9452
|
}
|
|
9135
9453
|
}
|
|
9136
9454
|
function accountRunCost(deps, run) {
|
|
9137
|
-
const dir =
|
|
9138
|
-
const files = unprocessedLogFiles(dir);
|
|
9139
|
-
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;
|
|
9140
9458
|
const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
|
|
9141
9459
|
const executor = deps.executor ?? resolveExecutor();
|
|
9142
9460
|
let added = 0;
|
|
@@ -9145,7 +9463,7 @@ function accountRunCost(deps, run) {
|
|
|
9145
9463
|
let durationMs = 0;
|
|
9146
9464
|
const processed = [];
|
|
9147
9465
|
for (const f of files) {
|
|
9148
|
-
const full =
|
|
9466
|
+
const full = join15(dir, f);
|
|
9149
9467
|
let raw = null;
|
|
9150
9468
|
try {
|
|
9151
9469
|
raw = readFileSync11(full, "utf8");
|
|
@@ -9174,7 +9492,7 @@ function accountRunCost(deps, run) {
|
|
|
9174
9492
|
}
|
|
9175
9493
|
for (const full of processed) {
|
|
9176
9494
|
try {
|
|
9177
|
-
|
|
9495
|
+
renameSync8(full, `${full}.done`);
|
|
9178
9496
|
} catch {
|
|
9179
9497
|
}
|
|
9180
9498
|
}
|
|
@@ -9182,20 +9500,34 @@ function accountRunCost(deps, run) {
|
|
|
9182
9500
|
function makeCostAccountant(deps) {
|
|
9183
9501
|
return (run) => accountRunCost(deps, run);
|
|
9184
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
|
+
}
|
|
9185
9516
|
|
|
9186
9517
|
// src/runOutcome.ts
|
|
9518
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "fs";
|
|
9519
|
+
import { join as join16 } from "path";
|
|
9187
9520
|
init_executors();
|
|
9188
|
-
import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
|
|
9189
|
-
import { join as join15 } from "path";
|
|
9190
9521
|
function endedItself(outcome) {
|
|
9191
9522
|
return outcome === "graceful" || outcome === "budget";
|
|
9192
9523
|
}
|
|
9193
9524
|
var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
|
|
9194
|
-
function classifyExit(runLogsDir,
|
|
9195
|
-
const dir =
|
|
9525
|
+
function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
|
|
9526
|
+
const dir = findRunLogDir(runLogsDir, runLogKeys);
|
|
9527
|
+
if (!dir) return "unknown";
|
|
9196
9528
|
let files;
|
|
9197
9529
|
try {
|
|
9198
|
-
files =
|
|
9530
|
+
files = readdirSync10(dir);
|
|
9199
9531
|
} catch {
|
|
9200
9532
|
return "unknown";
|
|
9201
9533
|
}
|
|
@@ -9209,7 +9541,7 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
|
|
|
9209
9541
|
if (!latest) return "unknown";
|
|
9210
9542
|
let raw;
|
|
9211
9543
|
try {
|
|
9212
|
-
raw = readFileSync12(
|
|
9544
|
+
raw = readFileSync12(join16(dir, latest.name), "utf8");
|
|
9213
9545
|
} catch {
|
|
9214
9546
|
return "unknown";
|
|
9215
9547
|
}
|
|
@@ -9217,9 +9549,9 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
|
|
|
9217
9549
|
if (!result) return "crash";
|
|
9218
9550
|
return result.stoppedByBudget ? "budget" : "graceful";
|
|
9219
9551
|
}
|
|
9220
|
-
function makeExitClassifier(runLogsDir, executor) {
|
|
9552
|
+
function makeExitClassifier(runLogsDir, runLogKeys, executor) {
|
|
9221
9553
|
const exec = executor ?? resolveExecutor();
|
|
9222
|
-
return (run) => classifyExit(runLogsDir, run.id, exec);
|
|
9554
|
+
return (run) => classifyExit(runLogsDir, runLogKeys(run.id), exec);
|
|
9223
9555
|
}
|
|
9224
9556
|
|
|
9225
9557
|
// src/orchestrator.ts
|
|
@@ -9227,20 +9559,20 @@ init_src();
|
|
|
9227
9559
|
init_git();
|
|
9228
9560
|
|
|
9229
9561
|
// src/deps.ts
|
|
9230
|
-
import { existsSync as
|
|
9231
|
-
import { join as
|
|
9562
|
+
import { existsSync as existsSync11, readdirSync as readdirSync12, readFileSync as readFileSync13 } from "fs";
|
|
9563
|
+
import { join as join19 } from "path";
|
|
9232
9564
|
import { execa } from "execa";
|
|
9233
9565
|
|
|
9234
9566
|
// src/prepareLock.ts
|
|
9235
9567
|
import { createHash } from "crypto";
|
|
9236
|
-
import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as
|
|
9237
|
-
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";
|
|
9238
9570
|
import { tmpdir } from "os";
|
|
9239
9571
|
var WAIT_TIMEOUT_MS = 5 * 6e4;
|
|
9240
9572
|
var POLL_MS = 300;
|
|
9241
9573
|
var STALE_MS = 20 * 6e4;
|
|
9242
9574
|
function machineLockPath(key, dir = tmpdir()) {
|
|
9243
|
-
return
|
|
9575
|
+
return join18(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
|
|
9244
9576
|
}
|
|
9245
9577
|
function defaultTryCreate(path, content) {
|
|
9246
9578
|
try {
|
|
@@ -9257,7 +9589,7 @@ function defaultTryCreate(path, content) {
|
|
|
9257
9589
|
}
|
|
9258
9590
|
function defaultAgeMs(path) {
|
|
9259
9591
|
try {
|
|
9260
|
-
return Date.now() -
|
|
9592
|
+
return Date.now() - statSync9(path).mtimeMs;
|
|
9261
9593
|
} catch {
|
|
9262
9594
|
return null;
|
|
9263
9595
|
}
|
|
@@ -9321,14 +9653,14 @@ function safeReadText(p) {
|
|
|
9321
9653
|
}
|
|
9322
9654
|
function safeListDirs(p) {
|
|
9323
9655
|
try {
|
|
9324
|
-
return
|
|
9656
|
+
return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9325
9657
|
} catch {
|
|
9326
9658
|
return [];
|
|
9327
9659
|
}
|
|
9328
9660
|
}
|
|
9329
9661
|
function safeListFiles(p) {
|
|
9330
9662
|
try {
|
|
9331
|
-
return
|
|
9663
|
+
return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
9332
9664
|
} catch {
|
|
9333
9665
|
return [];
|
|
9334
9666
|
}
|
|
@@ -9354,21 +9686,21 @@ function overridePlan(override) {
|
|
|
9354
9686
|
};
|
|
9355
9687
|
}
|
|
9356
9688
|
function detectPackageManager(worktree, io = {}) {
|
|
9357
|
-
const exists = io.exists ??
|
|
9689
|
+
const exists = io.exists ?? existsSync11;
|
|
9358
9690
|
const readText = io.readText ?? safeReadText;
|
|
9359
|
-
const hasPkgJson = exists(
|
|
9691
|
+
const hasPkgJson = exists(join19(worktree, "package.json"));
|
|
9360
9692
|
if (!hasPkgJson) {
|
|
9361
|
-
if (exists(
|
|
9693
|
+
if (exists(join19(worktree, "deno.json")) || exists(join19(worktree, "deno.jsonc")) || exists(join19(worktree, "deno.lock"))) return "deno";
|
|
9362
9694
|
return null;
|
|
9363
9695
|
}
|
|
9364
|
-
const pkg = readJson(readText,
|
|
9696
|
+
const pkg = readJson(readText, join19(worktree, "package.json"));
|
|
9365
9697
|
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
|
|
9366
9698
|
if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
|
|
9367
|
-
if (exists(
|
|
9368
|
-
if (exists(
|
|
9369
|
-
if (exists(
|
|
9370
|
-
if (exists(
|
|
9371
|
-
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";
|
|
9372
9704
|
return "npm";
|
|
9373
9705
|
}
|
|
9374
9706
|
function jsInstallCommand(pm) {
|
|
@@ -9421,7 +9753,7 @@ var ECOSYSTEM_MARKERS = [
|
|
|
9421
9753
|
// конфигурация — дело сборки, не подготовки
|
|
9422
9754
|
];
|
|
9423
9755
|
function detectPlan(worktree, io) {
|
|
9424
|
-
const exists = io.exists ??
|
|
9756
|
+
const exists = io.exists ?? existsSync11;
|
|
9425
9757
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
9426
9758
|
const pm = detectPackageManager(worktree, io);
|
|
9427
9759
|
if (pm) {
|
|
@@ -9429,7 +9761,7 @@ function detectPlan(worktree, io) {
|
|
|
9429
9761
|
// node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
|
|
9430
9762
|
// У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
|
|
9431
9763
|
// заново, но он дешёвый: тянет из того же общего кэша.
|
|
9432
|
-
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists:
|
|
9764
|
+
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join19(worktree, "node_modules"), source: "detect" }
|
|
9433
9765
|
];
|
|
9434
9766
|
for (const target of workspaceBuildTargets(worktree, io)) {
|
|
9435
9767
|
commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
|
|
@@ -9438,12 +9770,12 @@ function detectPlan(worktree, io) {
|
|
|
9438
9770
|
return { source: "detect", ecosystem: `node/${pm}`, commands };
|
|
9439
9771
|
}
|
|
9440
9772
|
for (const marker of ECOSYSTEM_MARKERS) {
|
|
9441
|
-
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));
|
|
9442
9774
|
if (!hit) continue;
|
|
9443
9775
|
let ecosystem = marker.ecosystem;
|
|
9444
9776
|
let command = marker.command;
|
|
9445
9777
|
for (const refine of marker.refine ?? []) {
|
|
9446
|
-
if (!exists(
|
|
9778
|
+
if (!exists(join19(worktree, refine.file))) continue;
|
|
9447
9779
|
ecosystem = refine.ecosystem;
|
|
9448
9780
|
command = refine.command;
|
|
9449
9781
|
break;
|
|
@@ -9500,12 +9832,12 @@ var CI_INSTALL_PATTERNS = [
|
|
|
9500
9832
|
function ciWorkflowPlan(worktree, io) {
|
|
9501
9833
|
const readText = io.readText ?? safeReadText;
|
|
9502
9834
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
9503
|
-
const dir =
|
|
9835
|
+
const dir = join19(worktree, ".github", "workflows");
|
|
9504
9836
|
const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
|
|
9505
9837
|
const seen = /* @__PURE__ */ new Set();
|
|
9506
9838
|
const commands = [];
|
|
9507
9839
|
for (const name of files.sort()) {
|
|
9508
|
-
const yaml = readText(
|
|
9840
|
+
const yaml = readText(join19(dir, name));
|
|
9509
9841
|
if (yaml === null) continue;
|
|
9510
9842
|
for (const raw of workflowRunLines(yaml)) {
|
|
9511
9843
|
const line = raw.replace(/\s+/g, " ").trim();
|
|
@@ -9520,7 +9852,7 @@ function ciWorkflowPlan(worktree, io) {
|
|
|
9520
9852
|
return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
|
|
9521
9853
|
}
|
|
9522
9854
|
function workspaceGlobs(worktree, readText) {
|
|
9523
|
-
const yaml = readText(
|
|
9855
|
+
const yaml = readText(join19(worktree, "pnpm-workspace.yaml"));
|
|
9524
9856
|
if (yaml !== null) {
|
|
9525
9857
|
const globs = [];
|
|
9526
9858
|
let inPackages = false;
|
|
@@ -9540,7 +9872,7 @@ function workspaceGlobs(worktree, readText) {
|
|
|
9540
9872
|
}
|
|
9541
9873
|
if (globs.length) return globs;
|
|
9542
9874
|
}
|
|
9543
|
-
const pkg = readJson(readText,
|
|
9875
|
+
const pkg = readJson(readText, join19(worktree, "package.json"));
|
|
9544
9876
|
const ws = pkg?.workspaces;
|
|
9545
9877
|
const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
9546
9878
|
return list.filter((g) => typeof g === "string");
|
|
@@ -9551,14 +9883,14 @@ function workspaceDirs(worktree, globs, io) {
|
|
|
9551
9883
|
if (glob.startsWith("!")) continue;
|
|
9552
9884
|
const star = glob.indexOf("*");
|
|
9553
9885
|
if (star === -1) {
|
|
9554
|
-
const dir =
|
|
9555
|
-
if (io.exists(
|
|
9886
|
+
const dir = join19(worktree, glob);
|
|
9887
|
+
if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
|
|
9556
9888
|
continue;
|
|
9557
9889
|
}
|
|
9558
|
-
const base3 =
|
|
9890
|
+
const base3 = join19(worktree, glob.slice(0, star).replace(/\/$/, ""));
|
|
9559
9891
|
for (const name of io.listDirs(base3)) {
|
|
9560
|
-
const dir =
|
|
9561
|
-
if (io.exists(
|
|
9892
|
+
const dir = join19(base3, name);
|
|
9893
|
+
if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
|
|
9562
9894
|
}
|
|
9563
9895
|
}
|
|
9564
9896
|
return [...dirs];
|
|
@@ -9579,18 +9911,18 @@ function entryPaths(pkg) {
|
|
|
9579
9911
|
return out;
|
|
9580
9912
|
}
|
|
9581
9913
|
function workspaceBuildTargets(worktree, io = {}) {
|
|
9582
|
-
const exists = io.exists ??
|
|
9914
|
+
const exists = io.exists ?? existsSync11;
|
|
9583
9915
|
const readText = io.readText ?? safeReadText;
|
|
9584
9916
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
9585
9917
|
const globs = workspaceGlobs(worktree, readText);
|
|
9586
9918
|
if (!globs.length) return [];
|
|
9587
9919
|
const pkgs = [];
|
|
9588
9920
|
for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
|
|
9589
|
-
const pkg = readJson(readText,
|
|
9921
|
+
const pkg = readJson(readText, join19(dir, "package.json"));
|
|
9590
9922
|
if (!pkg || typeof pkg.name !== "string") continue;
|
|
9591
9923
|
const scripts = pkg.scripts ?? {};
|
|
9592
9924
|
const entries = entryPaths(pkg);
|
|
9593
|
-
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)));
|
|
9594
9926
|
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
9595
9927
|
pkgs.push({ name: pkg.name, dir, deps, needsBuild });
|
|
9596
9928
|
}
|
|
@@ -9618,7 +9950,7 @@ function devDatabaseUrl(worktree, io = {}) {
|
|
|
9618
9950
|
const fromEnv = (io.env ?? process.env).DATABASE_URL;
|
|
9619
9951
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
9620
9952
|
for (const file of [".env", ".env.example"]) {
|
|
9621
|
-
const raw = readText(
|
|
9953
|
+
const raw = readText(join19(worktree, file));
|
|
9622
9954
|
if (raw === null) continue;
|
|
9623
9955
|
const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
|
|
9624
9956
|
const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
|
|
@@ -9634,11 +9966,11 @@ function isLocalDatabase(url) {
|
|
|
9634
9966
|
}
|
|
9635
9967
|
}
|
|
9636
9968
|
function prismaPackages(worktree, io) {
|
|
9637
|
-
const exists = io.exists ??
|
|
9969
|
+
const exists = io.exists ?? existsSync11;
|
|
9638
9970
|
const readText = io.readText ?? safeReadText;
|
|
9639
9971
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
9640
9972
|
const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
|
|
9641
|
-
return dirs.filter((dir) => exists(
|
|
9973
|
+
return dirs.filter((dir) => exists(join19(dir, "prisma", "schema.prisma")) && listDirs(join19(dir, "prisma", "migrations")).length > 0);
|
|
9642
9974
|
}
|
|
9643
9975
|
function jsExecBin(pm, command) {
|
|
9644
9976
|
switch (pm) {
|
|
@@ -9671,7 +10003,7 @@ function planPrepare(worktree, io = {}) {
|
|
|
9671
10003
|
return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
|
|
9672
10004
|
}
|
|
9673
10005
|
async function ensureWorktreeDeps(worktree, io = {}) {
|
|
9674
|
-
const exists = io.exists ??
|
|
10006
|
+
const exists = io.exists ?? existsSync11;
|
|
9675
10007
|
const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
|
|
9676
10008
|
const approve = io.approve ?? (() => true);
|
|
9677
10009
|
const plan = planPrepare(worktree, io);
|
|
@@ -9856,6 +10188,43 @@ function pruneDefers(episodes, live) {
|
|
|
9856
10188
|
for (const key of episodes.keys()) if (!live.has(key)) episodes.delete(key);
|
|
9857
10189
|
}
|
|
9858
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
|
+
|
|
9859
10228
|
// src/overmind.ts
|
|
9860
10229
|
init_git();
|
|
9861
10230
|
|
|
@@ -10122,11 +10491,16 @@ import { z } from "zod";
|
|
|
10122
10491
|
init_src();
|
|
10123
10492
|
|
|
10124
10493
|
// src/reviewCycle.ts
|
|
10494
|
+
init_src();
|
|
10125
10495
|
function reviewTransitionPatch(run, prUrl) {
|
|
10126
|
-
const isFixIteration = run.review_verdict === "changes";
|
|
10496
|
+
const isFixIteration = run.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run.verify_verdict));
|
|
10127
10497
|
if (isFixIteration) {
|
|
10128
10498
|
const cycle = (run.review_cycle ?? 0) + 1;
|
|
10129
|
-
return {
|
|
10499
|
+
return {
|
|
10500
|
+
isFixIteration: true,
|
|
10501
|
+
cycle,
|
|
10502
|
+
patch: { status: "review", pr_url: prUrl, review_cycle: cycle, review_verdict: null, verify_verdict: null }
|
|
10503
|
+
};
|
|
10130
10504
|
}
|
|
10131
10505
|
return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
|
|
10132
10506
|
}
|
|
@@ -11108,7 +11482,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11108
11482
|
};
|
|
11109
11483
|
const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
|
|
11110
11484
|
const recentFailed = allRuns.filter(
|
|
11111
|
-
(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)
|
|
11112
11486
|
);
|
|
11113
11487
|
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
11114
11488
|
candidates.push({
|
|
@@ -11167,6 +11541,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11167
11541
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
11168
11542
|
if (killedByOperator(run)) continue;
|
|
11169
11543
|
if (closedBeforeStart(run)) continue;
|
|
11544
|
+
if (isPreStartFailure(run)) continue;
|
|
11170
11545
|
const repeat = isRepeatDown(run);
|
|
11171
11546
|
candidates.push({
|
|
11172
11547
|
key: `run_failed:${run.id}`,
|
|
@@ -11181,6 +11556,23 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11181
11556
|
})
|
|
11182
11557
|
});
|
|
11183
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
|
+
}
|
|
11184
11576
|
const queuedCount = allRuns.filter((r) => r.status === "queued").length;
|
|
11185
11577
|
const activeDroneCount2 = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
11186
11578
|
if (queuedCount > 0 && activeDroneCount2 === 0) {
|
|
@@ -11198,6 +11590,31 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11198
11590
|
})
|
|
11199
11591
|
});
|
|
11200
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
|
+
}
|
|
11201
11618
|
if (opts.gateReason && queuedCount > 0) {
|
|
11202
11619
|
const reason = opts.gateReason;
|
|
11203
11620
|
candidates.push({
|
|
@@ -11282,6 +11699,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
11282
11699
|
for (const q of db.openQuestions()) {
|
|
11283
11700
|
const run = runsById.get(q.run_id);
|
|
11284
11701
|
if (!run) continue;
|
|
11702
|
+
if (isTerminalStatus(run.status)) continue;
|
|
11285
11703
|
candidates.push({
|
|
11286
11704
|
key: `question_unanswered:${q.id}`,
|
|
11287
11705
|
minDurationMs: thresholds.questionUnansweredMs,
|
|
@@ -11764,22 +12182,22 @@ function makeMergeGateResolver(deps) {
|
|
|
11764
12182
|
}
|
|
11765
12183
|
|
|
11766
12184
|
// src/envGate.ts
|
|
11767
|
-
import { existsSync as
|
|
11768
|
-
import { join as
|
|
12185
|
+
import { existsSync as existsSync14 } from "fs";
|
|
12186
|
+
import { join as join25 } from "path";
|
|
11769
12187
|
import { createConnection } from "net";
|
|
11770
12188
|
import { execa as execa2 } from "execa";
|
|
11771
12189
|
|
|
11772
12190
|
// src/platform.ts
|
|
11773
|
-
import { existsSync as
|
|
11774
|
-
import { delimiter, join as
|
|
12191
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
12192
|
+
import { delimiter, join as join24 } from "path";
|
|
11775
12193
|
function resolveCommandPath(name, env = process.env, platform = process.platform) {
|
|
11776
|
-
if (name.includes("/") || name.includes("\\")) return
|
|
12194
|
+
if (name.includes("/") || name.includes("\\")) return existsSync13(name) ? name : null;
|
|
11777
12195
|
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
11778
12196
|
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
11779
12197
|
for (const dir of dirs) {
|
|
11780
12198
|
for (const ext of exts) {
|
|
11781
|
-
const candidate =
|
|
11782
|
-
if (
|
|
12199
|
+
const candidate = join24(dir, name + ext);
|
|
12200
|
+
if (existsSync13(candidate)) return candidate;
|
|
11783
12201
|
}
|
|
11784
12202
|
}
|
|
11785
12203
|
return null;
|
|
@@ -11862,8 +12280,8 @@ var COMPOSE_MARKERS = [
|
|
|
11862
12280
|
"compose.yaml"
|
|
11863
12281
|
];
|
|
11864
12282
|
function detectRequirements(repo, io = {}) {
|
|
11865
|
-
const exists = io.exists ??
|
|
11866
|
-
const marker = COMPOSE_MARKERS.find((f) => exists(
|
|
12283
|
+
const exists = io.exists ?? existsSync14;
|
|
12284
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join25(repo, f)));
|
|
11867
12285
|
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
11868
12286
|
}
|
|
11869
12287
|
function resolveRequirements(repo, declared, io = {}) {
|
|
@@ -11999,8 +12417,8 @@ import { basename as basename3 } from "path";
|
|
|
11999
12417
|
import { serve as serve2 } from "@hono/node-server";
|
|
12000
12418
|
|
|
12001
12419
|
// src/diff.ts
|
|
12002
|
-
import { statSync as
|
|
12003
|
-
import { join as
|
|
12420
|
+
import { statSync as statSync10 } from "fs";
|
|
12421
|
+
import { join as join26 } from "path";
|
|
12004
12422
|
import { $ as $4 } from "execa";
|
|
12005
12423
|
var MAX_FILES = 200;
|
|
12006
12424
|
var MAX_LINES_PER_FILE = 1500;
|
|
@@ -12143,7 +12561,7 @@ async function resolveBase(wt, baseBranch) {
|
|
|
12143
12561
|
}
|
|
12144
12562
|
function mtime(path) {
|
|
12145
12563
|
try {
|
|
12146
|
-
return
|
|
12564
|
+
return statSync10(path).mtimeMs;
|
|
12147
12565
|
} catch {
|
|
12148
12566
|
return 0;
|
|
12149
12567
|
}
|
|
@@ -12168,7 +12586,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
12168
12586
|
files.push(pf);
|
|
12169
12587
|
}
|
|
12170
12588
|
}
|
|
12171
|
-
files.sort((a, b) => mtime(
|
|
12589
|
+
files.sort((a, b) => mtime(join26(worktree, b.path)) - mtime(join26(worktree, a.path)));
|
|
12172
12590
|
const truncatedFiles = files.length > MAX_FILES;
|
|
12173
12591
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
12174
12592
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -12201,7 +12619,7 @@ async function changedPaths(worktree, opts = {}) {
|
|
|
12201
12619
|
// package.json
|
|
12202
12620
|
var package_default = {
|
|
12203
12621
|
name: "@taskless-app/swarm",
|
|
12204
|
-
version: "0.6.
|
|
12622
|
+
version: "0.6.15",
|
|
12205
12623
|
type: "module",
|
|
12206
12624
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
12207
12625
|
keywords: [
|
|
@@ -12251,7 +12669,7 @@ var package_default = {
|
|
|
12251
12669
|
execa: "^10.0.0",
|
|
12252
12670
|
zod: "3.25.76"
|
|
12253
12671
|
},
|
|
12254
|
-
gitHead: "
|
|
12672
|
+
gitHead: "5bc8e0c067de58fd72b0eb434e80ad29c7a00223"
|
|
12255
12673
|
};
|
|
12256
12674
|
|
|
12257
12675
|
// src/version.ts
|
|
@@ -13884,10 +14302,10 @@ async function checkDaemonVersion(start, opts = {}) {
|
|
|
13884
14302
|
|
|
13885
14303
|
// src/hostCommands.ts
|
|
13886
14304
|
init_src();
|
|
13887
|
-
import { readFileSync as readFileSync18, renameSync as
|
|
13888
|
-
import { join as
|
|
14305
|
+
import { readFileSync as readFileSync18, renameSync as renameSync9, writeFileSync as writeFileSync13 } from "fs";
|
|
14306
|
+
import { join as join27 } from "path";
|
|
13889
14307
|
function hostCommandStatePath(home) {
|
|
13890
|
-
return
|
|
14308
|
+
return join27(home, "host-command.json");
|
|
13891
14309
|
}
|
|
13892
14310
|
function readState(path) {
|
|
13893
14311
|
try {
|
|
@@ -13899,7 +14317,7 @@ function readState(path) {
|
|
|
13899
14317
|
function writeState(path, state) {
|
|
13900
14318
|
try {
|
|
13901
14319
|
writeFileSync13(`${path}.tmp`, JSON.stringify(state));
|
|
13902
|
-
|
|
14320
|
+
renameSync9(`${path}.tmp`, path);
|
|
13903
14321
|
} catch {
|
|
13904
14322
|
}
|
|
13905
14323
|
}
|
|
@@ -14061,8 +14479,8 @@ async function doRestart(cmd, opts, note) {
|
|
|
14061
14479
|
|
|
14062
14480
|
// src/selfUpdate.ts
|
|
14063
14481
|
init_git();
|
|
14064
|
-
import { existsSync as
|
|
14065
|
-
import { join as
|
|
14482
|
+
import { existsSync as existsSync15 } from "fs";
|
|
14483
|
+
import { join as join28 } from "path";
|
|
14066
14484
|
import { $ as $5 } from "execa";
|
|
14067
14485
|
var DAEMON_PACKAGE = "@taskless-app/swarm";
|
|
14068
14486
|
var defaultRun = async (cmd, args, cwd) => {
|
|
@@ -14075,7 +14493,7 @@ function tail(text, max = 400) {
|
|
|
14075
14493
|
}
|
|
14076
14494
|
async function runSelfUpdate(io) {
|
|
14077
14495
|
const run = io.run ?? defaultRun;
|
|
14078
|
-
const exists = io.exists ??
|
|
14496
|
+
const exists = io.exists ?? existsSync15;
|
|
14079
14497
|
const repo = io.repo;
|
|
14080
14498
|
await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
|
|
14081
14499
|
const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
|
|
@@ -14104,7 +14522,7 @@ async function runSelfUpdate(io) {
|
|
|
14104
14522
|
};
|
|
14105
14523
|
}
|
|
14106
14524
|
await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
|
|
14107
|
-
if (!exists(
|
|
14525
|
+
if (!exists(join28(repo, "pnpm-lock.yaml"))) {
|
|
14108
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" };
|
|
14109
14527
|
}
|
|
14110
14528
|
const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
|
|
@@ -14162,6 +14580,47 @@ function makeOvermindNarrator() {
|
|
|
14162
14580
|
};
|
|
14163
14581
|
}
|
|
14164
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
|
+
|
|
14165
14624
|
// src/probe.ts
|
|
14166
14625
|
init_executors();
|
|
14167
14626
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -14222,18 +14681,18 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
14222
14681
|
}
|
|
14223
14682
|
|
|
14224
14683
|
// src/anomalyState.ts
|
|
14225
|
-
import { existsSync as
|
|
14226
|
-
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";
|
|
14227
14686
|
var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
14228
14687
|
var MAX_RESOLVED = 200;
|
|
14229
14688
|
function emptyState() {
|
|
14230
14689
|
return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
|
|
14231
14690
|
}
|
|
14232
14691
|
function anomalyStatePath(home) {
|
|
14233
|
-
return
|
|
14692
|
+
return join29(home, "anomaly-state.json");
|
|
14234
14693
|
}
|
|
14235
14694
|
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
14236
|
-
const exists = io.exists ??
|
|
14695
|
+
const exists = io.exists ?? existsSync16;
|
|
14237
14696
|
const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
|
|
14238
14697
|
const path = anomalyStatePath(home);
|
|
14239
14698
|
if (!exists(path)) return emptyState();
|
|
@@ -14265,7 +14724,7 @@ function saveAnomalyState(home, state, io = {}) {
|
|
|
14265
14724
|
const path = anomalyStatePath(home);
|
|
14266
14725
|
const writeFile3 = io.writeFile ?? ((p, data) => {
|
|
14267
14726
|
writeFileSync14(`${p}.tmp`, data);
|
|
14268
|
-
|
|
14727
|
+
renameSync10(`${p}.tmp`, p);
|
|
14269
14728
|
});
|
|
14270
14729
|
writeFile3(path, JSON.stringify(state, null, 2));
|
|
14271
14730
|
}
|
|
@@ -14591,6 +15050,9 @@ function short(files, n = 3) {
|
|
|
14591
15050
|
return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
|
|
14592
15051
|
}
|
|
14593
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
|
+
}
|
|
14594
15056
|
function decideConflict({ model, candidate, active }) {
|
|
14595
15057
|
if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
|
|
14596
15058
|
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
@@ -14598,23 +15060,16 @@ function decideConflict({ model, candidate, active }) {
|
|
|
14598
15060
|
if (!predicted.length) return { kind: "unknown" };
|
|
14599
15061
|
if (!active.length) return { kind: "clear" };
|
|
14600
15062
|
const spine = new Set(model.spine);
|
|
14601
|
-
const candidateSpine =
|
|
14602
|
-
if (candidateSpine.
|
|
14603
|
-
const
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
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})`
|
|
14608
|
-
};
|
|
14609
|
-
}
|
|
14610
|
-
for (const w of active) {
|
|
14611
|
-
if (!w.actual) continue;
|
|
14612
|
-
const theirSpine = w.files.filter((f) => spine.has(f));
|
|
14613
|
-
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;
|
|
14614
15069
|
return {
|
|
14615
15070
|
kind: "wait",
|
|
14616
15071
|
blockedBy: w.run,
|
|
14617
|
-
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)")
|
|
14618
15073
|
};
|
|
14619
15074
|
}
|
|
14620
15075
|
}
|
|
@@ -15212,6 +15667,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
15212
15667
|
"3. `/verify` \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B: \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0444\u0438\u0447\u0443 \u043D\u0430 \u0436\u0438\u0432\u043E\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 (\u043A\u0430\u043A \u0435\u0433\u043E \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u2014 \u0432 \u0441\u043A\u0438\u043B\u043B\u0430\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F) \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E \u043A\u043E\u043C\u043F\u0438\u043B\u0438\u0440\u0443\u0435\u0442\u0441\u044F.",
|
|
15213
15668
|
`4. \u0421\u043A\u0440\u0438\u043D\u044B mobile+desktop \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B, \u041E\u0414\u041D\u041E\u0419 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: \`swarm shot <url> <out-prefix>\` (headless Chromium \u0441\u0430\u043C \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u043E\u0431\u0430 \u0432\u044C\u044E\u043F\u043E\u0440\u0442\u0430, \u0441\u043D\u0438\u043C\u0430\u0435\u0442 \u0438 \u0432\u044B\u0445\u043E\u0434\u0438\u0442 \u2014 \u043D\u0435 \u0434\u0435\u0440\u0436\u0438\u0442 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C). \u041F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u2014 \u043E\u0431\u0430 \u0444\u0430\u0439\u043B\u0430 \u0418\u0414\u0423\u0422 \u0412 \u0417\u0410\u0414\u0410\u0427\u0423: \u043F\u0435\u0440\u0435\u0434\u0430\u0439 \u0438\u0445 \u043F\u0443\u0442\u0438 \u0432 \`done(pr_url, summary, screenshots: ["\u2026-desktop.png", "\u2026-mobile.png"])\`, \u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043B\u043E\u0436\u0438\u0442 \u0438\u0445 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438 \u043A \u0442\u0432\u043E\u0435\u043C\u0443 \u043E\u0442\u0447\u0451\u0442\u0443 (TASWM-294). \u0427\u0435\u043B\u043E\u0432\u0435\u043A \u0441\u043C\u043E\u0442\u0440\u0438\u0442 \u0442\u0440\u0435\u043A\u0435\u0440, \u0430 \u043D\u0435 \u0433\u0438\u0442\u0445\u0430\u0431: \u0437\u0430\u0434\u0430\u0447\u0430 \u0431\u0435\u0437 \u0441\u043A\u0440\u0438\u043D\u043E\u0432 = \u043D\u0435\u0434\u043E\u043A\u0430\u0437\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430. \u0412 PR \u2014 \u0414\u041E\u041F\u041E\u041B\u041D\u0418\u0422\u0415\u041B\u042C\u041D\u041E, \u043F\u043E \u0436\u0435\u043B\u0430\u043D\u0438\u044E; \u0432\u043C\u0435\u0441\u0442\u043E \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u043D\u0435\u0442. \u23F1 \u0425\u0430\u0440\u0434-\u043B\u0438\u043C\u0438\u0442 \u043D\u0430 \u0432\u0435\u0441\u044C \u0448\u0430\u0433 \u0441\u043A\u0440\u0438\u043D\u043E\u0432 \u2014 5 \u043C\u0438\u043D\u0443\u0442. \u041D\u0415 \u0433\u043E\u043D\u044F\u0439 Playwright/\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0439 Chrome \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043F\u043E \u0448\u0430\u0433\u0430\u043C (\u043E\u0442\u043A\u0440\u044B\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2192\u043A\u043B\u0438\u043A\u043D\u0443\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2026) \u2014 \u0442\u0430\u043A \u0434\u0440\u043E\u043D \u0414\u0412\u0410\u0416\u0414\u042B \u0437\u0430\u043B\u0438\u043F\u0430\u043B \u043D\u0430 \u044D\u0442\u043E\u043C (TASWM-30, >30 \u043C\u0438\u043D stall \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u0434\u0430\u043B\u044C\u0448\u0435) \u0432\u043C\u0435\u0441\u0442\u043E \u0442\u043E\u0433\u043E \u0447\u0442\u043E\u0431\u044B \u0441\u0434\u0430\u0442\u044C \u0433\u043E\u0442\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443. \u041D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043B\u0438\u043C\u0438\u0442 \u0438\u043B\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442 \u2014 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u043D\u044F\u043B, \u0438\u043B\u0438 \u043F\u0440\u044F\u043C\u043E \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 PR/\u0437\u0430\u0434\u0430\u0447\u0435 \xAB\u0441\u043A\u0440\u0438\u043D\u044B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\xBB \u0438 \u0421\u0420\u0410\u0417\u0423 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u2014 \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0433\u0440\u044B\u0432\u0430\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043F\u043E \u043A\u0440\u0443\u0433\u0443.`,
|
|
15214
15669
|
"4a. \u0421\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0417\u0410 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0431\u0435\u0437 \u0433\u043E\u0442\u043E\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0434\u0430\u0451\u0442 \u044D\u043A\u0440\u0430\u043D \u043B\u043E\u0433\u0438\u043D\u0430, \u0430 \u043D\u0435 \u0446\u0435\u043B\u0435\u0432\u043E\u0439 \u044D\u043A\u0440\u0430\u043D \u2014 \u0441\u043D\u0438\u043C\u0430\u0442\u044C \u0435\u0451 \u043D\u0443\u0436\u043D\u043E \u0441 `--storage-state <\u0444\u0430\u0439\u043B>` (\u043A\u0430\u043A \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u0442\u0430\u043A\u043E\u0439 \u0444\u0430\u0439\u043B \u0432 \u044D\u0442\u043E\u043C \u043F\u0440\u043E\u0435\u043A\u0442\u0435 \u2014 \u0432 \u0435\u0433\u043E \u0441\u043A\u0438\u043B\u043B\u0430\u0445). \u041F\u043E\u0440\u044F\u0434\u043E\u043A \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u0432\u0430\u0436\u0435\u043D: `--storage-state` \u0411\u0415\u0417 \u043F\u0440\u0435\u0434\u0448\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0435\u0433\u043E `<out-prefix>` \u043C\u043E\u043B\u0447\u0430 \u0441\u044A\u0435\u0434\u0430\u0435\u0442 \u0435\u0433\u043E \u043C\u0435\u0441\u0442\u043E, \u0438 \u0444\u043B\u0430\u0433 \u043D\u0438\u043A\u043E\u0433\u0434\u0430 \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u0441\u044F (\u0441\u043C. shotCli.ts parseShotArgs) \u2014 \u0435\u0441\u043B\u0438 \u043D\u0430 \u0441\u043D\u0438\u043C\u043A\u0435 \u044D\u043A\u0440\u0430\u043D \u043B\u043E\u0433\u0438\u043D\u0430 \u0432\u043C\u0435\u0441\u0442\u043E \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F, \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u044D\u0442\u043E.",
|
|
15670
|
+
"4b. \u0421\u041D\u0418\u041C\u0410\u0419 \u041F\u0420\u041E\u0414\u0423\u041A\u0422, \u0410 \u041D\u0415 \u041F\u0420\u0415\u0412\u042C\u042E-\u0421\u0422\u0420\u0410\u041D\u0418\u0426\u0423 (TASWM-404). \u0414\u0435\u043C\u043E-\u0440\u043E\u0443\u0442 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u0430 (`/dev/*`, \u043F\u0435\u0441\u043E\u0447\u043D\u0438\u0446\u0430, \u0441\u0442\u043E\u0440\u0438 \u0432 \u043C\u043E\u0431\u0438\u043B\u044C\u043D\u043E\u043C \u0444\u0440\u0435\u0439\u043C\u0435) \u2014 \u041D\u0415 \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u043C\u043E\u0431\u0438\u043B\u044C\u043D\u043E\u0439 \u0432\u0451\u0440\u0441\u0442\u043A\u0438: \u0443 \u0442\u0430\u043A\u043E\u0439 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0441\u0432\u043E\u044F \u043E\u0431\u043E\u043B\u043E\u0447\u043A\u0430, \u0435\u0451 \u043C\u0435\u043D\u044E \u043E\u0431\u044B\u0447\u043D\u043E \u043D\u0435 \u0441\u0432\u043E\u0440\u0430\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0443\u0437\u043A\u043E\u043C \u044D\u043A\u0440\u0430\u043D\u0435, \u0438 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u043C\u0443 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u043F\u043E\u043B\u043E\u0441\u0430 \u0432 \u043F\u0430\u0440\u0443 \u0441\u043E\u0442\u0435\u043D \u043F\u0438\u043A\u0441\u0435\u043B\u0435\u0439 \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u0439 \u0448\u0438\u0440\u0438\u043D\u044B. \u041F\u043E \u0442\u0430\u043A\u043E\u043C\u0443 \u0441\u043D\u0438\u043C\u043A\u0443 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u043D\u0438 \u0448\u0438\u0440\u0438\u043D\u044B \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432, \u043D\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u0448\u0430\u043F\u043A\u0438, \u043D\u0438 \u043D\u0430\u043F\u043E\u043B\u0437\u0430\u043D\u0438\u044F \u043B\u0438\u043F\u043A\u0438\u0445 \u043F\u043E\u0434\u043F\u0438\u0441\u0435\u0439 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442 \u0412\u0421\u0415\u0413\u0414\u0410 \u0438 \u043D\u0435 \u043B\u043E\u0432\u0438\u0442 \u043D\u0438\u0447\u0435\u0433\u043E (\u0442\u0430\u043A \u0441\u0434\u0430\u043B\u0438 \u0441\u043B\u043E\u043C\u0430\u043D\u043D\u0443\u044E \u043C\u043E\u0431\u0438\u043B\u0443 \u0432 TASWM-403, \u0441\u043A\u0440\u0438\u043D \u043F\u0440\u0438 \u044D\u0442\u043E\u043C \u0431\u044B\u043B \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D). \u0421\u043D\u0438\u043C\u0430\u0439 \u0420\u0415\u0410\u041B\u042C\u041D\u042B\u0419 \u0440\u043E\u0443\u0442 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u043E\u0434 \u0436\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439; \u043A\u0430\u043A \u044D\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 \u0432 \u044D\u0442\u043E\u043C \u043F\u0440\u043E\u0435\u043A\u0442\u0435 \u2014 \u0432 \u0435\u0433\u043E \u0441\u043A\u0438\u043B\u043B\u0430\u0445. \u041F\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u043E\u043D\u043D\u043E \u0440\u043E\u0432\u043D\u043E \u0442\u043E\u0433\u0434\u0430, \u043A\u043E\u0433\u0434\u0430 \u043F\u0440\u0430\u0432\u0438\u0448\u044C \u0441\u0430\u043C \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442 \u0432\u043D\u0435 \u043E\u0431\u043E\u043B\u043E\u0447\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F.",
|
|
15215
15671
|
"\u0427\u0438\u0441\u0442\u043E \u0431\u044D\u043A\u0435\u043D\u0434/\u043B\u043E\u0433\u0438\u043A\u0443/API/\u0434\u0435\u043C\u043E\u043D/CI/\u0442\u0438\u043F\u044B \u2014 \u0448\u0430\u0433\u0438 3\u20134 \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438 (\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u041D\u0415 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0439 \u0438 \u0441\u043A\u0440\u0438\u043D\u044B \u043D\u0435 \u0441\u043D\u0438\u043C\u0430\u0439): \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u0442\u0435\u0441\u0442\u044B (\u0448\u0430\u0433 1) + e2e \u0432 CI \u044D\u0442\u043E \u0443\u0436\u0435 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u044E\u0442; \u044D\u043A\u043E\u043D\u043E\u043C\u0438\u043C \u0432\u0440\u0435\u043C\u044F \u0438 \u0442\u043E\u043A\u0435\u043D\u044B.",
|
|
15216
15672
|
"\u0422\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E \u0437\u043E\u0432\u0438 done(pr_url), \u0438 \u0432 \u0441\u0434\u0430\u0447\u0435 \u043F\u043E\u043A\u0430\u0436\u0438 \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E (\u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u0432\u044B\u0432\u043E\u0434/\u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u043E\u0435 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435), \u0430 \u043D\u0435 \xAB\u0434\u043E\u043B\u0436\u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C\xBB.",
|
|
15217
15673
|
"\u0421\u043A\u0438\u043B\u043B\u044B \u043F\u0440\u0438\u0445\u043E\u0434\u044F\u0442 \u043A \u0442\u0435\u0431\u0435 \u0414\u0412\u0423\u041C\u042F \u043D\u0430\u0431\u043E\u0440\u0430\u043C\u0438, \u0438 \u043E\u0431\u0430 \u0443\u0436\u0435 \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D\u044B (\u0441\u043F\u0430\u0432\u043D \u0438\u0437\u043E\u043B\u0438\u0440\u043E\u0432\u0430\u043D \u043E\u0442 \u044E\u0437\u0435\u0440\u0441\u043A\u043E\u0433\u043E ~/.claude, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u041B\u0418\u0427\u041D\u042B\u0415 \u0441\u043A\u0438\u043B\u043B\u044B \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u0442\u0435\u0431\u0435 \u043D\u0435 \u0432\u0438\u0434\u043D\u044B). \u2460 \u0420\u043E\u0439\u043D\u044B\u0435 `swarm-drone:*` \u0435\u0434\u0443\u0442 \u0441 \u0420\u041E\u0415\u041C \u0432 \u043B\u044E\u0431\u043E\u0439 \u043F\u0440\u043E\u0435\u043A\u0442 \u2014 \u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430, \u0432\u0435\u0440\u043D\u0430\u044F \u0432\u0435\u0437\u0434\u0435: swarm-save-often (\u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0438\u0440\u0430\u0435\u0442 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C), swarm-merge-main (\u0431\u0430\u0437\u043E\u0432\u0443\u044E \u0432\u0435\u0442\u043A\u0443 \u0432\u043B\u0438\u0432\u0430\u0442\u044C, \u0430 \u043D\u0435 \u0440\u0435\u0431\u0435\u0439\u0437\u0438\u0442\u044C \u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u0443\u044E), swarm-await-process (\u0436\u0434\u0430\u0442\u044C \u0443\u0441\u043B\u043E\u0432\u0438\u0435, \u043D\u0435 sleep), swarm-verify-green (\u0447\u0442\u043E \u0441\u0447\u0438\u0442\u0430\u0442\u044C \u0437\u0435\u043B\u0451\u043D\u044B\u043C), systematic-debugging (\u043F\u0435\u0440\u0432\u043E\u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0434\u043E \u0444\u0438\u043A\u0441\u0430). \u2461 \u0421\u043A\u0438\u043B\u043B\u044B \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F (.claude/skills, \u0441\u043E\u0441\u0442\u0430\u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043E\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430) \u2014 \u0437\u043D\u0430\u043D\u0438\u044F \u043E\u0431 \u044D\u0442\u043E\u043C \u043F\u0440\u043E\u0435\u043A\u0442\u0435: \u043A\u0430\u043A \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0441\u0442\u0435\u043A, \u043A\u0430\u043A \u0433\u043E\u043D\u044F\u0442\u044C \u0442\u0435\u0441\u0442\u044B \u0438 \u0447\u0442\u043E \u0437\u0434\u0435\u0441\u044C \u0437\u0435\u043B\u0451\u043D\u043E\u0435, UI-\u043F\u0440\u0430\u0432\u0438\u043B\u0430. \u041F\u043E\u043B\u044C\u0437\u0443\u0439\u0441\u044F \u043E\u0431\u043E\u0438\u043C\u0438, \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0437\u043E\u0431\u0440\u0435\u0442\u0430\u0439; \u043D\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u0437\u043D\u0430\u043D\u0438\u044F \u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0435 \u2014 \u044D\u0442\u043E \u043F\u0440\u043E\u0431\u0435\u043B \u0435\u0433\u043E \u0441\u043A\u0438\u043B\u043B\u043E\u0432, \u0441\u043A\u0430\u0436\u0438 note(), \u0430 \u043D\u0435 \u0432\u044B\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B."
|
|
@@ -15225,7 +15681,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
15225
15681
|
}
|
|
15226
15682
|
return parts.join("\n");
|
|
15227
15683
|
}
|
|
15228
|
-
var HERO_CHARTER_PATH =
|
|
15684
|
+
var HERO_CHARTER_PATH = join30(dirname4(THIS_FILE), "..", "docs", "HERO.md");
|
|
15229
15685
|
function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
|
|
15230
15686
|
const name = run.name || `#${run.id}`;
|
|
15231
15687
|
const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
|
|
@@ -15525,7 +15981,7 @@ async function wakeRun(deps, runId) {
|
|
|
15525
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` };
|
|
15526
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` };
|
|
15527
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` };
|
|
15528
|
-
const worktreeExists = deps.worktreeExists ??
|
|
15984
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
15529
15985
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
15530
15986
|
const wt = run.worktree_path;
|
|
15531
15987
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -15610,9 +16066,9 @@ function repoHasCi(deps, run) {
|
|
|
15610
16066
|
function repoHasCiWorkflows(worktreePath, repo) {
|
|
15611
16067
|
for (const base3 of [worktreePath, repo]) {
|
|
15612
16068
|
if (!base3) continue;
|
|
15613
|
-
const dir =
|
|
16069
|
+
const dir = join30(base3, ".github", "workflows");
|
|
15614
16070
|
try {
|
|
15615
|
-
if (
|
|
16071
|
+
if (readdirSync13(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
|
|
15616
16072
|
} catch {
|
|
15617
16073
|
}
|
|
15618
16074
|
}
|
|
@@ -15761,7 +16217,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
|
|
|
15761
16217
|
return true;
|
|
15762
16218
|
}
|
|
15763
16219
|
function worktreeAndTranscriptAlive(deps, worktree) {
|
|
15764
|
-
const exists = deps.worktreeExists ??
|
|
16220
|
+
const exists = deps.worktreeExists ?? existsSync17;
|
|
15765
16221
|
const transcript = deps.hasTranscript ?? executorHasSession;
|
|
15766
16222
|
return exists(worktree) && transcript(worktree);
|
|
15767
16223
|
}
|
|
@@ -16064,6 +16520,15 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16064
16520
|
// причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
|
|
16065
16521
|
});
|
|
16066
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
|
+
}
|
|
16067
16532
|
const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
16068
16533
|
if (mergedUrl) {
|
|
16069
16534
|
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
@@ -16074,7 +16539,11 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16074
16539
|
}
|
|
16075
16540
|
const depVerdicts = deps.db.depVerdicts(run.depends_on);
|
|
16076
16541
|
const unmet = depVerdicts.filter((d) => !d.met);
|
|
16077
|
-
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
|
+
}
|
|
16078
16547
|
if (!await envGateAllows(deps, run)) return "skipped";
|
|
16079
16548
|
if (!await projectQuotaGateAllows(deps, run)) return "skipped";
|
|
16080
16549
|
const prodOverride = await prodOverrideReason(deps, run, ["queued"]);
|
|
@@ -16084,6 +16553,8 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16084
16553
|
}
|
|
16085
16554
|
noteExhaustedDeps(deps, run, depVerdicts);
|
|
16086
16555
|
await fillSuggestedArea(deps, run);
|
|
16556
|
+
const repoGap = unresolvedRepoReason(run.repo, deps.reposServed ? deps.reposServed() : []);
|
|
16557
|
+
if (repoGap) return await failBeforeStart(deps, run, repoGap);
|
|
16087
16558
|
try {
|
|
16088
16559
|
await deps.spawn(run);
|
|
16089
16560
|
if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
|
|
@@ -16100,10 +16571,32 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
16100
16571
|
return "skipped";
|
|
16101
16572
|
}
|
|
16102
16573
|
if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
|
|
16103
|
-
deps
|
|
16104
|
-
|
|
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}`);
|
|
16105
16583
|
return "failed";
|
|
16106
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";
|
|
16107
16600
|
}
|
|
16108
16601
|
async function batchSpawnQueue(deps, droneCap) {
|
|
16109
16602
|
for (const run of orderQueue(deps.db.listRuns("queued"))) {
|
|
@@ -16125,7 +16618,7 @@ async function resumeWatchdogParked(deps, droneCap) {
|
|
|
16125
16618
|
}
|
|
16126
16619
|
if (!candidate) return;
|
|
16127
16620
|
if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
|
|
16128
|
-
const worktreeExists = deps.worktreeExists ??
|
|
16621
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
16129
16622
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
16130
16623
|
const wt = candidate.worktree_path;
|
|
16131
16624
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -16294,7 +16787,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
16294
16787
|
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
16295
16788
|
if (!deps.respawn) return;
|
|
16296
16789
|
const wt = resumeCandidate.worktree_path;
|
|
16297
|
-
const worktreeExists = deps.worktreeExists ??
|
|
16790
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
16298
16791
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
16299
16792
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
16300
16793
|
const mode = canContinue ? "continue" : "fresh";
|
|
@@ -16563,7 +17056,7 @@ function buildEscalationCandidates(db) {
|
|
|
16563
17056
|
}
|
|
16564
17057
|
async function tickOnce(deps) {
|
|
16565
17058
|
const now = deps.now ? deps.now() : Date.now();
|
|
16566
|
-
const worktreeExists = deps.worktreeExists ??
|
|
17059
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
16567
17060
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
16568
17061
|
deps.sampleHost?.();
|
|
16569
17062
|
if (deps.rehydrate) {
|
|
@@ -16804,7 +17297,7 @@ async function tickOnce(deps) {
|
|
|
16804
17297
|
} else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
|
|
16805
17298
|
await handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap);
|
|
16806
17299
|
}
|
|
16807
|
-
if (run.review_verdict === "changes" && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
|
|
17300
|
+
if ((run.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run.verify_verdict))) && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
|
|
16808
17301
|
continue;
|
|
16809
17302
|
}
|
|
16810
17303
|
const reviewerGatesOk = !st.conflicting && !st.isDraft && !!deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
|
|
@@ -16894,6 +17387,13 @@ async function tickOnce(deps) {
|
|
|
16894
17387
|
liveChildPpids = null;
|
|
16895
17388
|
logTransientSpawnError("ps (livePpids)", e);
|
|
16896
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
|
+
}
|
|
16897
17397
|
for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
|
|
16898
17398
|
const alive = run.pid != null && deps.isAlive(run.pid);
|
|
16899
17399
|
if (!alive && deps.db.takeRun && !await deps.db.takeRun(run.id)) continue;
|
|
@@ -17119,6 +17619,16 @@ async function tickOnce(deps) {
|
|
|
17119
17619
|
if (deps.respawn && deps.resourcesOk && !stormActive) {
|
|
17120
17620
|
await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
|
|
17121
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
|
+
}
|
|
17122
17632
|
if (deps.anomalies && !deps.control?.paused) {
|
|
17123
17633
|
const az = deps.anomalies;
|
|
17124
17634
|
let ready = [];
|
|
@@ -17227,8 +17737,14 @@ async function replayDecision(deps, qid, chosen, by = "human") {
|
|
|
17227
17737
|
repo: old.repo,
|
|
17228
17738
|
area: old.area,
|
|
17229
17739
|
decision: { question: q.text, options: q.options, chosen, prior: q.answer, priorState: old.status },
|
|
17230
|
-
workspace_id: old.workspace_id
|
|
17740
|
+
workspace_id: old.workspace_id,
|
|
17231
17741
|
// переигровка той же задачи — тот же воркспейс (TASWM-288)
|
|
17742
|
+
// TASWM-451: та же ЗАДАЧА — те же модели. Переигровка это продолжение той же работы, а не
|
|
17743
|
+
// новая: без переноса любой вопрос дрона оператору молча возвращал бы задачу на дефолт
|
|
17744
|
+
// роли, и выбор постановщика («эту делаем сильной моделью») терялся бы ровно там, где
|
|
17745
|
+
// работа оказалась сложной. Тот же довод, что у area и workspace_id строкой выше.
|
|
17746
|
+
model: old.model,
|
|
17747
|
+
review_model: old.review_model
|
|
17232
17748
|
});
|
|
17233
17749
|
deps.db.updateRun(old.id, { status: "superseded" });
|
|
17234
17750
|
deps.db.supersede(old.id, fresh.id, by);
|
|
@@ -17256,10 +17772,15 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
17256
17772
|
area: reviewRun.area,
|
|
17257
17773
|
// держим area-lock, пока чиним — не начинаем конфликтующее в зоне
|
|
17258
17774
|
resolves_run_id: reviewRun.id,
|
|
17259
|
-
workspace_id: reviewRun.workspace_id
|
|
17775
|
+
workspace_id: reviewRun.workspace_id,
|
|
17260
17776
|
// та же задача, тот же воркспейс (TASWM-288)
|
|
17777
|
+
// TASWM-451: резолвер доделывает ТУ ЖЕ работу — разводит конфликт в её же ветке, и
|
|
17778
|
+
// модель ему нужна та же, что выбрали задаче. Модель проверки несём дальше, чтобы её не
|
|
17779
|
+
// потеряли проверяющие, которых поднимут уже по прогону резолвера.
|
|
17780
|
+
model: reviewRun.model,
|
|
17781
|
+
review_model: reviewRun.review_model
|
|
17261
17782
|
});
|
|
17262
|
-
const wt =
|
|
17783
|
+
const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
|
|
17263
17784
|
try {
|
|
17264
17785
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
17265
17786
|
await deps.git.setWorktreeIdentity(wt, resolver.name);
|
|
@@ -17304,6 +17825,11 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
17304
17825
|
task_slug: reviewRun.task_slug,
|
|
17305
17826
|
repo,
|
|
17306
17827
|
kind: "reviewer",
|
|
17828
|
+
// TASWM-451: ЧЕМ проверять — решение постановщика, приехавшее с задачей на прогон
|
|
17829
|
+
// исполнителя. Пин, а не «спросим настройки на спавне»: ревьюер заводится локально, и
|
|
17830
|
+
// задача ему недоступна. Пусто в задаче → null → дефолт роли (модель проверки).
|
|
17831
|
+
model: reviewRun.review_model,
|
|
17832
|
+
review_model: reviewRun.review_model,
|
|
17307
17833
|
reviews_run_id: reviewRun.id,
|
|
17308
17834
|
review_cycle: reviewRun.review_cycle,
|
|
17309
17835
|
// штамп цикла (авто-ревью v2, TASWM-162): reviewerCountFor текущего цикла
|
|
@@ -17311,7 +17837,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
17311
17837
|
// (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
|
|
17312
17838
|
workspace_id: reviewRun.workspace_id
|
|
17313
17839
|
});
|
|
17314
|
-
const wt =
|
|
17840
|
+
const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
17315
17841
|
try {
|
|
17316
17842
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
17317
17843
|
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
@@ -17376,12 +17902,17 @@ async function spawnVerifier(deps, verifiedRun, files) {
|
|
|
17376
17902
|
task_slug: verifiedRun.task_slug,
|
|
17377
17903
|
repo,
|
|
17378
17904
|
kind: "verifier",
|
|
17905
|
+
// TASWM-451: та же модель проверки, что у ревьюера, и из того же источника — задачи.
|
|
17906
|
+
// Две проверки одного PR должны стоить одинаково, иначе одна из них незаметно уедет
|
|
17907
|
+
// на модель исполнителя (ровно это и стоило $45.94 на TAS-524).
|
|
17908
|
+
model: verifiedRun.review_model,
|
|
17909
|
+
review_model: verifiedRun.review_model,
|
|
17379
17910
|
verifies_run_id: verifiedRun.id,
|
|
17380
17911
|
review_cycle: verifiedRun.review_cycle,
|
|
17381
17912
|
// штамп цикла: фикс-итерацию проверяет свежий верификатор
|
|
17382
17913
|
workspace_id: verifiedRun.workspace_id
|
|
17383
17914
|
});
|
|
17384
|
-
const wt =
|
|
17915
|
+
const wt = join30(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
|
|
17385
17916
|
try {
|
|
17386
17917
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
17387
17918
|
await deps.git.setWorktreeIdentity(wt, verifier.name);
|
|
@@ -17419,7 +17950,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
17419
17950
|
]);
|
|
17420
17951
|
const base3 = effectiveBaseBranch(run.base_branch, projectBase);
|
|
17421
17952
|
const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
|
|
17422
|
-
if (!
|
|
17953
|
+
if (!existsSync17(wt)) {
|
|
17423
17954
|
const branch = runToSpawn.branch || runToSpawn.task_slug;
|
|
17424
17955
|
const source = await addWorktree(repo, wt, branch, {
|
|
17425
17956
|
...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
|
|
@@ -17446,7 +17977,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
17446
17977
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
17447
17978
|
}
|
|
17448
17979
|
async function syncCrashedOnStartup(deps) {
|
|
17449
|
-
const worktreeExists = deps.worktreeExists ??
|
|
17980
|
+
const worktreeExists = deps.worktreeExists ?? existsSync17;
|
|
17450
17981
|
let orphaned = 0;
|
|
17451
17982
|
let merged = 0;
|
|
17452
17983
|
let requeued = 0;
|
|
@@ -17535,10 +18066,12 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
17535
18066
|
roots: scanRootsList,
|
|
17536
18067
|
maxDepth: SWARM_SCAN_DEPTH,
|
|
17537
18068
|
cachePath: REPO_MAP_PATH,
|
|
18069
|
+
// TASWM-418: кэш обязан протухать — иначе новый клон на машине не находится никогда.
|
|
18070
|
+
scanTtlMs: SWARM_REPO_SCAN_TTL_MS,
|
|
17538
18071
|
state: repoMap,
|
|
17539
18072
|
log: (m) => console.log(m)
|
|
17540
18073
|
});
|
|
17541
|
-
|
|
18074
|
+
let repoRegistry = await rebuildRegistry();
|
|
17542
18075
|
if (repoRegistry.length === 0) {
|
|
17543
18076
|
console.warn(
|
|
17544
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: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
|
|
@@ -17568,8 +18101,13 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
17568
18101
|
// Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
|
|
17569
18102
|
// и список для claim/heartbeat, и обратную карту owner/repo → путь.
|
|
17570
18103
|
refreshRepoMap: async () => {
|
|
17571
|
-
|
|
17572
|
-
|
|
18104
|
+
repoRegistry = await rebuildRegistry();
|
|
18105
|
+
client.setRepoRegistry(repoRegistry);
|
|
18106
|
+
},
|
|
18107
|
+
// TASWM-422: что машина обслуживает ПРЯМО СЕЙЧАС — читается из ЖИВОГО реестра (он
|
|
18108
|
+
// пересобирается тиком выше), чтобы причина «клона нет на этой машине» называла
|
|
18109
|
+
// актуальный список, а не снимок на старте демона.
|
|
18110
|
+
reposServed: () => repoRegistry.map((e) => e.repoFullName)
|
|
17573
18111
|
};
|
|
17574
18112
|
}
|
|
17575
18113
|
var crashGuardsInstalled = false;
|
|
@@ -17589,22 +18127,22 @@ async function migrateWorktreesAtStartup() {
|
|
|
17589
18127
|
legacyDir: WORKTREES_LEGACY_DIR,
|
|
17590
18128
|
targetDir: WORKTREES_DIR,
|
|
17591
18129
|
envOverride: process.env.SWARM_WORKTREES || "",
|
|
17592
|
-
exists: (d) =>
|
|
18130
|
+
exists: (d) => existsSync17(d),
|
|
17593
18131
|
isNonEmptyDir: (d) => {
|
|
17594
18132
|
try {
|
|
17595
|
-
return
|
|
18133
|
+
return readdirSync13(d).length > 0;
|
|
17596
18134
|
} catch {
|
|
17597
18135
|
return false;
|
|
17598
18136
|
}
|
|
17599
18137
|
},
|
|
17600
18138
|
liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
|
|
17601
|
-
rename: (from, to) =>
|
|
18139
|
+
rename: (from, to) => renameSync11(from, to),
|
|
17602
18140
|
repairMoved: (t) => repairMovedWorktrees(t),
|
|
17603
18141
|
log: (m) => console.log(m)
|
|
17604
18142
|
});
|
|
17605
18143
|
} catch (e) {
|
|
17606
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);
|
|
17607
|
-
return
|
|
18145
|
+
return existsSync17(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
|
|
17608
18146
|
}
|
|
17609
18147
|
}
|
|
17610
18148
|
async function reportWorkspaceAccess(tl, db) {
|
|
@@ -17624,6 +18162,14 @@ async function runDaemon(opts) {
|
|
|
17624
18162
|
const daemonStart = captureDaemonStart();
|
|
17625
18163
|
const latestRelease = fetchLatestVersion();
|
|
17626
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
|
+
}
|
|
17627
18173
|
if (!opts.repo) {
|
|
17628
18174
|
const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
|
|
17629
18175
|
if (remembered) opts.repo = remembered;
|
|
@@ -17634,7 +18180,8 @@ async function runDaemon(opts) {
|
|
|
17634
18180
|
host,
|
|
17635
18181
|
repoMap: repoMapState,
|
|
17636
18182
|
refreshRepoMap: rebuildRepoMap,
|
|
17637
|
-
prodMcp
|
|
18183
|
+
prodMcp,
|
|
18184
|
+
reposServed
|
|
17638
18185
|
} = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
|
|
17639
18186
|
await preflightAuth();
|
|
17640
18187
|
const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
|
|
@@ -17680,6 +18227,8 @@ async function runDaemon(opts) {
|
|
|
17680
18227
|
overmindEntry: THIS_FILE,
|
|
17681
18228
|
dbPath: DB_PATH,
|
|
17682
18229
|
runLogsDir: RUN_LOGS_DIR,
|
|
18230
|
+
// TASWM-419: чем именуется каталог лога прогона — стабильные ключи из стора, не номер.
|
|
18231
|
+
runLogKeys: (run) => db.runLogKeys(run.id),
|
|
17683
18232
|
// Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
|
|
17684
18233
|
executor: EXECUTOR,
|
|
17685
18234
|
droneIsolation: DRONE_ISOLATION,
|
|
@@ -17746,7 +18295,18 @@ async function runDaemon(opts) {
|
|
|
17746
18295
|
control.state ?? null,
|
|
17747
18296
|
lastOwnCapacity,
|
|
17748
18297
|
hostResourcesForProd(),
|
|
17749
|
-
{ 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 }
|
|
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,
|
|
18300
|
+
// TASWM-393: ВХОД в Taskless — та же природа, что пауза и шторм: состояние
|
|
18301
|
+
// машины, о котором знает только она. Пульс идёт и без входа, поэтому «на
|
|
18302
|
+
// связи» без этого поля читалось как «возьмёт работу» (12.08 — шесть прогонов
|
|
18303
|
+
// в очереди у машины, которая снаружи выглядела здоровой). Адрес шлём
|
|
18304
|
+
// ФАКТИЧЕСКИЙ (web.port), а не зашитый: порт настраивается.
|
|
18305
|
+
// signedInToTaskless(), а НЕ !authStateLine(): отказ — это «токен есть, но его
|
|
18306
|
+
// отвергают», а 12.08 входа не делали вовсе, и по отказу машина числилась бы
|
|
18307
|
+
// вошедшей. Адрес — WEB_PORT (он же SWARM_WEB_PORT из окружения, на нём и
|
|
18308
|
+
// поднимается морда): зашитый 7420 отправил бы человека туда, где ничего нет.
|
|
18309
|
+
{ signedIn: signedInToTaskless(), webUrl: `http://127.0.0.1:${WEB_PORT}` }
|
|
17750
18310
|
);
|
|
17751
18311
|
if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
|
|
17752
18312
|
if (reply.commands.length) onHostCommands(reply.commands);
|
|
@@ -17755,7 +18315,13 @@ async function runDaemon(opts) {
|
|
|
17755
18315
|
{
|
|
17756
18316
|
runLogsDir: RUN_LOGS_DIR,
|
|
17757
18317
|
daemonLogPath: DAEMON_LOG_PATH,
|
|
17758
|
-
|
|
18318
|
+
// TASWM-419: каталог лога адресуется стабильными ключами прогона, а не его
|
|
18319
|
+
// локальным номером — номера переиспользуются между жизнями зеркала, и
|
|
18320
|
+
// оператору приезжал хвост чужого прогона.
|
|
18321
|
+
runLogKeys: (prodRunId) => {
|
|
18322
|
+
const local = client.localRunIdForProd(prodRunId);
|
|
18323
|
+
return local == null ? [] : db.runLogKeys(local);
|
|
18324
|
+
},
|
|
17759
18325
|
deliver: (requestId, payload) => client.deliverLog(requestId, payload)
|
|
17760
18326
|
},
|
|
17761
18327
|
reply.logRequests
|
|
@@ -17813,6 +18379,7 @@ async function runDaemon(opts) {
|
|
|
17813
18379
|
questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
|
|
17814
18380
|
outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS,
|
|
17815
18381
|
resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
|
|
18382
|
+
depMissingMs: SWARM_ANOMALY_DEP_MISSING_MS,
|
|
17816
18383
|
runDownWindowMs: SWARM_ANOMALY_RUN_DOWN_WINDOW_MS
|
|
17817
18384
|
},
|
|
17818
18385
|
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
@@ -18072,6 +18639,30 @@ async function runDaemon(opts) {
|
|
|
18072
18639
|
taskClosed: (slug, ws) => tl.forWorkspace(ws).getTask(slug).then((t) => t ? isClosingTaskCategory(t.statusCategory) : null)
|
|
18073
18640
|
} : {},
|
|
18074
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
|
+
},
|
|
18075
18666
|
// TASWM-272: отчёт/вердикт-компенсация при усыновлении мёртвого дрона — реальный
|
|
18076
18667
|
// TasklessClient.addComment + gh/git хелперы. Без tl (нет токена) ключи не задаём —
|
|
18077
18668
|
// postAdoptionReport/postScrapedVerdictReport становятся no-op, как раньше.
|
|
@@ -18093,9 +18684,12 @@ async function runDaemon(opts) {
|
|
|
18093
18684
|
// TASWM-364: swarmModels — ТОТ ЖЕ снимок, что уехал в launcher: цена токенов считается
|
|
18094
18685
|
// по модели, на которой прогон реально работал.
|
|
18095
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 }),
|
|
18096
18690
|
// Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
|
|
18097
18691
|
// (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
|
|
18098
|
-
exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
|
|
18692
|
+
exitOutcome: makeExitClassifier(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
|
|
18099
18693
|
// Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
|
|
18100
18694
|
livePpids,
|
|
18101
18695
|
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
@@ -18242,7 +18836,7 @@ async function runDaemon(opts) {
|
|
|
18242
18836
|
mode,
|
|
18243
18837
|
nudge
|
|
18244
18838
|
),
|
|
18245
|
-
worktreeExists: (p) =>
|
|
18839
|
+
worktreeExists: (p) => existsSync17(p),
|
|
18246
18840
|
hasTranscript: executorHasSession,
|
|
18247
18841
|
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
18248
18842
|
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|