@taskless-app/swarm 0.6.26 → 0.6.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +271 -218
- package/dist/overmind.js +494 -241
- package/package.json +2 -2
package/dist/overmind.js
CHANGED
|
@@ -309,52 +309,6 @@ var init_timeline = __esm({
|
|
|
309
309
|
}
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
-
// ../swarm-core/src/reviewPhase.ts
|
|
313
|
-
function computeReviewPhase(input) {
|
|
314
|
-
const { reviewVerdict, ciRed } = input;
|
|
315
|
-
if (reviewVerdict === "approve" && !ciRed) {
|
|
316
|
-
return { kind: "ready", label: REVIEW_PHASE_TEXT.ready, ready: true };
|
|
317
|
-
}
|
|
318
|
-
if (ciRed) {
|
|
319
|
-
const jobs = (input.ciFailing ?? []).filter((j) => j && j !== "?");
|
|
320
|
-
const label3 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
|
|
321
|
-
return { kind: "ci_fixing", label: label3, ready: false };
|
|
322
|
-
}
|
|
323
|
-
if (reviewVerdict === "changes") {
|
|
324
|
-
return { kind: "changes", label: REVIEW_PHASE_TEXT.changes, ready: false };
|
|
325
|
-
}
|
|
326
|
-
if (input.reviewerName) {
|
|
327
|
-
return { kind: "in_review", label: `\u041D\u0430 \u0440\u0435\u0432\u044C\u044E \u0443 ${input.reviewerName}`, ready: false };
|
|
328
|
-
}
|
|
329
|
-
if (input.reviewerDead) {
|
|
330
|
-
return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
|
|
331
|
-
}
|
|
332
|
-
const p = input.ciProgress;
|
|
333
|
-
if (p && p.total > 0 && p.pass >= p.total) {
|
|
334
|
-
return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
|
|
335
|
-
}
|
|
336
|
-
const label2 = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
|
|
337
|
-
return { kind: "checks", label: label2, ready: false };
|
|
338
|
-
}
|
|
339
|
-
var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
|
|
340
|
-
var init_reviewPhase = __esm({
|
|
341
|
-
"../swarm-core/src/reviewPhase.ts"() {
|
|
342
|
-
"use strict";
|
|
343
|
-
REVIEW_PHASE_TEXT = {
|
|
344
|
-
checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
|
|
345
|
-
awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
|
|
346
|
-
ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
|
|
347
|
-
in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
|
|
348
|
-
changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
|
|
349
|
-
ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
|
|
350
|
-
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"
|
|
351
|
-
};
|
|
352
|
-
PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
|
|
353
|
-
(a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
|
|
358
312
|
// ../swarm-core/src/verifyVerdict.ts
|
|
359
313
|
function isVerifyVerdict(v) {
|
|
360
314
|
return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
|
|
@@ -402,6 +356,68 @@ var init_verifyVerdict = __esm({
|
|
|
402
356
|
}
|
|
403
357
|
});
|
|
404
358
|
|
|
359
|
+
// ../swarm-core/src/reviewPhase.ts
|
|
360
|
+
function computeReviewPhase(input) {
|
|
361
|
+
const { reviewVerdict, ciRed } = input;
|
|
362
|
+
const verifyVerdict = input.verifyVerdict ?? null;
|
|
363
|
+
if (!ciRed && isNegativeVerifyVerdict(verifyVerdict)) {
|
|
364
|
+
const kind = verifyVerdict === "found_issues" ? "verify_found" : "verify_not_reproduced";
|
|
365
|
+
return { kind, label: REVIEW_PHASE_TEXT[kind], ready: false };
|
|
366
|
+
}
|
|
367
|
+
if (reviewVerdict === "approve" && !ciRed) {
|
|
368
|
+
if (verifyVerdict === "confirmed" || input.verifyExpected === false) {
|
|
369
|
+
return { kind: "ready", label: REVIEW_PHASE_TEXT.ready, ready: true };
|
|
370
|
+
}
|
|
371
|
+
if (input.verifierDead) {
|
|
372
|
+
return { kind: "verify_dead", label: REVIEW_PHASE_TEXT.verify_dead, ready: true };
|
|
373
|
+
}
|
|
374
|
+
return { kind: "awaiting_verify", label: REVIEW_PHASE_TEXT.awaiting_verify, ready: false };
|
|
375
|
+
}
|
|
376
|
+
if (ciRed) {
|
|
377
|
+
const jobs = (input.ciFailing ?? []).filter((j) => j && j !== "?");
|
|
378
|
+
const label3 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
|
|
379
|
+
return { kind: "ci_fixing", label: label3, ready: false };
|
|
380
|
+
}
|
|
381
|
+
if (reviewVerdict === "changes") {
|
|
382
|
+
return { kind: "changes", label: REVIEW_PHASE_TEXT.changes, ready: false };
|
|
383
|
+
}
|
|
384
|
+
if (input.reviewerName) {
|
|
385
|
+
return { kind: "in_review", label: `\u041D\u0430 \u0440\u0435\u0432\u044C\u044E \u0443 ${input.reviewerName}`, ready: false };
|
|
386
|
+
}
|
|
387
|
+
if (input.reviewerDead) {
|
|
388
|
+
return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
|
|
389
|
+
}
|
|
390
|
+
const p = input.ciProgress;
|
|
391
|
+
if (p && p.total > 0 && p.pass >= p.total) {
|
|
392
|
+
return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
|
|
393
|
+
}
|
|
394
|
+
const label2 = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
|
|
395
|
+
return { kind: "checks", label: label2, ready: false };
|
|
396
|
+
}
|
|
397
|
+
var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
|
|
398
|
+
var init_reviewPhase = __esm({
|
|
399
|
+
"../swarm-core/src/reviewPhase.ts"() {
|
|
400
|
+
"use strict";
|
|
401
|
+
init_verifyVerdict();
|
|
402
|
+
REVIEW_PHASE_TEXT = {
|
|
403
|
+
checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
|
|
404
|
+
awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
|
|
405
|
+
ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
|
|
406
|
+
in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
|
|
407
|
+
changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
|
|
408
|
+
verify_found: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0430\u0448\u0451\u043B \u2014 \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438",
|
|
409
|
+
verify_not_reproduced: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0451\u043B \u2014 \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438",
|
|
410
|
+
awaiting_verify: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u043C \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E",
|
|
411
|
+
verify_dead: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0435 \u0434\u0430\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
|
|
412
|
+
ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
|
|
413
|
+
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"
|
|
414
|
+
};
|
|
415
|
+
PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
|
|
416
|
+
(a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
405
421
|
// ../swarm-core/src/mergeGate.ts
|
|
406
422
|
function computeMergeGate(input) {
|
|
407
423
|
if (!input.hasRun) {
|
|
@@ -1709,6 +1725,57 @@ var init_zoneMemory = __esm({
|
|
|
1709
1725
|
}
|
|
1710
1726
|
});
|
|
1711
1727
|
|
|
1728
|
+
// ../swarm-core/src/taskPrs.ts
|
|
1729
|
+
function normalizePrUrl(raw) {
|
|
1730
|
+
const s = (raw ?? "").trim();
|
|
1731
|
+
if (!s) return null;
|
|
1732
|
+
const m = s.match(/^(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/i);
|
|
1733
|
+
if (!m) return s.replace(/\/+$/, "").toLowerCase();
|
|
1734
|
+
return `github.com/${m[1]}/${m[2]}/pull/${m[3]}`.toLowerCase();
|
|
1735
|
+
}
|
|
1736
|
+
function samePr(a, b) {
|
|
1737
|
+
const x = normalizePrUrl(a);
|
|
1738
|
+
const y = normalizePrUrl(b);
|
|
1739
|
+
return x != null && y != null && x === y;
|
|
1740
|
+
}
|
|
1741
|
+
function prBelongsToTask(slug, pr) {
|
|
1742
|
+
const s = (slug ?? "").trim().toLowerCase();
|
|
1743
|
+
if (!s) return false;
|
|
1744
|
+
const branch = (pr.headRefName ?? "").trim().toLowerCase();
|
|
1745
|
+
if (branch === s || branch.startsWith(s) && /^[-/_.]/.test(branch.slice(s.length))) return true;
|
|
1746
|
+
const title = (pr.title ?? "").toLowerCase();
|
|
1747
|
+
if (!title.includes(s)) return false;
|
|
1748
|
+
const at = title.indexOf(s);
|
|
1749
|
+
const before = at > 0 ? title[at - 1] : "";
|
|
1750
|
+
const after = title[at + s.length] ?? "";
|
|
1751
|
+
return !/[a-z0-9]/.test(before) && !/[a-z0-9-]/.test(after);
|
|
1752
|
+
}
|
|
1753
|
+
function decideTaskWork(facts) {
|
|
1754
|
+
const candidate = (facts.mergedCandidate ?? "").trim() || null;
|
|
1755
|
+
const open = [];
|
|
1756
|
+
for (const raw of facts.openPrs ?? []) {
|
|
1757
|
+
const url = (raw ?? "").trim();
|
|
1758
|
+
if (!url) continue;
|
|
1759
|
+
if (!open.some((u) => samePr(u, url))) open.push(url);
|
|
1760
|
+
}
|
|
1761
|
+
if (open.length > 0) return { kind: "open", prs: open, candidate: candidate && !open.some((u) => samePr(u, candidate)) ? candidate : null };
|
|
1762
|
+
if (!candidate) return { kind: "unmerged" };
|
|
1763
|
+
return { kind: "merged", pr: candidate };
|
|
1764
|
+
}
|
|
1765
|
+
function formatMergedDuplicateReason(pr) {
|
|
1766
|
+
return `PR \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D (${pr}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D-\u0434\u0443\u0431\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430/\u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0438 (TASWM-206)`;
|
|
1767
|
+
}
|
|
1768
|
+
function formatOpenPrHold(v) {
|
|
1769
|
+
const list = v.prs.join(", ");
|
|
1770
|
+
const tail2 = v.candidate ? ` (\u0441\u043C\u0435\u0440\u0436\u0435\u043D ${v.candidate}, \u043D\u043E \u044D\u0442\u043E \u043D\u0435 \u0432\u0441\u044F \u0440\u0430\u0431\u043E\u0442\u0430)` : "";
|
|
1771
|
+
return `\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u043E\u0441\u0442\u0430\u043B\u0438\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 PR: ${list}${tail2} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0432\u043B\u0438\u0442\u0430 \u0446\u0435\u043B\u0438\u043A\u043E\u043C, \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u043C \u0438 \u043F\u0440\u043E\u0433\u043E\u043D \u0434\u0443\u0431\u043B\u0435\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u043C (TASWM-515)`;
|
|
1772
|
+
}
|
|
1773
|
+
var init_taskPrs = __esm({
|
|
1774
|
+
"../swarm-core/src/taskPrs.ts"() {
|
|
1775
|
+
"use strict";
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1712
1779
|
// ../swarm-core/src/index.ts
|
|
1713
1780
|
var init_src = __esm({
|
|
1714
1781
|
"../swarm-core/src/index.ts"() {
|
|
@@ -1743,6 +1810,7 @@ var init_src = __esm({
|
|
|
1743
1810
|
init_effort();
|
|
1744
1811
|
init_queuePlan();
|
|
1745
1812
|
init_zoneMemory();
|
|
1813
|
+
init_taskPrs();
|
|
1746
1814
|
}
|
|
1747
1815
|
});
|
|
1748
1816
|
|
|
@@ -3024,76 +3092,12 @@ var init_storm = __esm({
|
|
|
3024
3092
|
}
|
|
3025
3093
|
});
|
|
3026
3094
|
|
|
3027
|
-
// src/heartbeat.ts
|
|
3028
|
-
import { readFileSync as readFileSync6, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
3029
|
-
function writeHeartbeat(path, now) {
|
|
3030
|
-
const tmp = `${path}.tmp`;
|
|
3031
|
-
writeFileSync6(tmp, String(now));
|
|
3032
|
-
renameSync4(tmp, path);
|
|
3033
|
-
}
|
|
3034
|
-
function readHeartbeat(path) {
|
|
3035
|
-
try {
|
|
3036
|
-
const ms = Number(readFileSync6(path, "utf8").trim());
|
|
3037
|
-
return Number.isFinite(ms) ? ms : null;
|
|
3038
|
-
} catch {
|
|
3039
|
-
return null;
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
function overmindAlive(path, now, staleMs) {
|
|
3043
|
-
const ts = readHeartbeat(path);
|
|
3044
|
-
if (ts == null) return false;
|
|
3045
|
-
return now - ts < staleMs;
|
|
3046
|
-
}
|
|
3047
|
-
var init_heartbeat = __esm({
|
|
3048
|
-
"src/heartbeat.ts"() {
|
|
3049
|
-
"use strict";
|
|
3050
|
-
}
|
|
3051
|
-
});
|
|
3052
|
-
|
|
3053
|
-
// src/logNoise.ts
|
|
3054
|
-
function timesWord(n) {
|
|
3055
|
-
const tens = n % 100;
|
|
3056
|
-
if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
|
|
3057
|
-
return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
|
|
3058
|
-
}
|
|
3059
|
-
function makeRepeatFilter(opts = {}) {
|
|
3060
|
-
const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
|
|
3061
|
-
const streaks = /* @__PURE__ */ new Map();
|
|
3062
|
-
return {
|
|
3063
|
-
say(key, message, now) {
|
|
3064
|
-
const prev = streaks.get(key);
|
|
3065
|
-
if (!prev || prev.message !== message) {
|
|
3066
|
-
const tail2 = prev && prev.hidden > 0 ? ` (\u0434\u043E \u044D\u0442\u043E\u0433\u043E \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 \u043C\u043E\u043B\u0447\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u043B\u043E\u0441\u044C \u0435\u0449\u0451 ${prev.hidden} ${timesWord(prev.hidden)})` : "";
|
|
3067
|
-
streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
|
|
3068
|
-
return `${message}${tail2}`;
|
|
3069
|
-
}
|
|
3070
|
-
prev.hidden += 1;
|
|
3071
|
-
if (now - prev.saidAt < remindAfterMs) return null;
|
|
3072
|
-
const hidden = prev.hidden;
|
|
3073
|
-
const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
|
|
3074
|
-
prev.saidAt = now;
|
|
3075
|
-
prev.hidden = 0;
|
|
3076
|
-
return `${message} \u2014 \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 ${hidden} ${timesWord(hidden)} \u043F\u043E\u0434\u0440\u044F\u0434, \u0442\u044F\u043D\u0435\u0442\u0441\u044F ${minutes} \u043C\u0438\u043D`;
|
|
3077
|
-
},
|
|
3078
|
-
forget(key) {
|
|
3079
|
-
streaks.delete(key);
|
|
3080
|
-
}
|
|
3081
|
-
};
|
|
3082
|
-
}
|
|
3083
|
-
var DEFAULT_REMIND_MS;
|
|
3084
|
-
var init_logNoise = __esm({
|
|
3085
|
-
"src/logNoise.ts"() {
|
|
3086
|
-
"use strict";
|
|
3087
|
-
DEFAULT_REMIND_MS = 10 * 6e4;
|
|
3088
|
-
}
|
|
3089
|
-
});
|
|
3090
|
-
|
|
3091
3095
|
// src/agentToken.ts
|
|
3092
|
-
import { readFileSync as
|
|
3093
|
-
import { join as
|
|
3096
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
3097
|
+
import { join as join7 } from "path";
|
|
3094
3098
|
function loadAgentToken() {
|
|
3095
3099
|
try {
|
|
3096
|
-
const s = JSON.parse(
|
|
3100
|
+
const s = JSON.parse(readFileSync5(STORE_PATH, "utf8"));
|
|
3097
3101
|
return s.token ? s : null;
|
|
3098
3102
|
} catch {
|
|
3099
3103
|
return null;
|
|
@@ -3146,7 +3150,7 @@ var init_agentToken = __esm({
|
|
|
3146
3150
|
"src/agentToken.ts"() {
|
|
3147
3151
|
"use strict";
|
|
3148
3152
|
init_config();
|
|
3149
|
-
STORE_PATH =
|
|
3153
|
+
STORE_PATH = join7(SWARM_HOME, "agent.json");
|
|
3150
3154
|
base = (api) => api.replace(/\/$/, "");
|
|
3151
3155
|
TOKEN_PROBE_TIMEOUT_MS = 8e3;
|
|
3152
3156
|
}
|
|
@@ -3187,6 +3191,142 @@ var init_linkFailure = __esm({
|
|
|
3187
3191
|
}
|
|
3188
3192
|
});
|
|
3189
3193
|
|
|
3194
|
+
// src/toolFailure.ts
|
|
3195
|
+
function trimDetail(s) {
|
|
3196
|
+
const t = s.trim();
|
|
3197
|
+
return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
|
|
3198
|
+
}
|
|
3199
|
+
function classifyToolFailure(raw) {
|
|
3200
|
+
if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
|
|
3201
|
+
if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
|
|
3202
|
+
if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
|
|
3203
|
+
return TOOL_FAILURE.TOOL_ERROR;
|
|
3204
|
+
}
|
|
3205
|
+
function classifyThrown(e) {
|
|
3206
|
+
const { kind, detail } = explainLinkFailure(e);
|
|
3207
|
+
const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
|
|
3208
|
+
// «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
|
|
3209
|
+
// и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
|
|
3210
|
+
classifyToolFailure(detail)
|
|
3211
|
+
);
|
|
3212
|
+
return { code, detail };
|
|
3213
|
+
}
|
|
3214
|
+
function describeToolFailure(f) {
|
|
3215
|
+
const text = TOOL_FAILURE_TEXT[f.code];
|
|
3216
|
+
const detail = trimDetail(f.detail);
|
|
3217
|
+
return detail ? `${text} (${detail})` : text;
|
|
3218
|
+
}
|
|
3219
|
+
var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
|
|
3220
|
+
var init_toolFailure = __esm({
|
|
3221
|
+
"src/toolFailure.ts"() {
|
|
3222
|
+
"use strict";
|
|
3223
|
+
init_linkFailure();
|
|
3224
|
+
TOOL_FAILURE = {
|
|
3225
|
+
/** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
|
|
3226
|
+
NOT_FOUND: "not_found",
|
|
3227
|
+
/** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
|
|
3228
|
+
FORBIDDEN: "forbidden",
|
|
3229
|
+
/** Токен роя не состоит в воркспейсе, которым искали. */
|
|
3230
|
+
WORKSPACE_MISMATCH: "workspace_mismatch",
|
|
3231
|
+
/** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
|
|
3232
|
+
UNREACHABLE: "unreachable",
|
|
3233
|
+
/** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
|
|
3234
|
+
UNAUTHORIZED: "unauthorized",
|
|
3235
|
+
/** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
|
|
3236
|
+
TOOL_ERROR: "tool_error",
|
|
3237
|
+
/** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
|
|
3238
|
+
EMPTY: "empty"
|
|
3239
|
+
};
|
|
3240
|
+
TOOL_FAILURE_TEXT = {
|
|
3241
|
+
[TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
|
|
3242
|
+
[TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
|
|
3243
|
+
[TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
|
|
3244
|
+
[TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
|
|
3245
|
+
[TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
|
|
3246
|
+
[TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
|
|
3247
|
+
[TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
|
|
3248
|
+
};
|
|
3249
|
+
TOOL_FAILURE_RETRIABLE = {
|
|
3250
|
+
[TOOL_FAILURE.NOT_FOUND]: false,
|
|
3251
|
+
[TOOL_FAILURE.FORBIDDEN]: false,
|
|
3252
|
+
[TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
|
|
3253
|
+
[TOOL_FAILURE.UNREACHABLE]: true,
|
|
3254
|
+
// Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
|
|
3255
|
+
[TOOL_FAILURE.UNAUTHORIZED]: false,
|
|
3256
|
+
[TOOL_FAILURE.TOOL_ERROR]: false,
|
|
3257
|
+
[TOOL_FAILURE.EMPTY]: false
|
|
3258
|
+
};
|
|
3259
|
+
FAILURE_DETAIL_MAX = 300;
|
|
3260
|
+
NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
|
|
3261
|
+
FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
|
|
3262
|
+
MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
// src/heartbeat.ts
|
|
3267
|
+
import { readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
3268
|
+
function writeHeartbeat(path, now) {
|
|
3269
|
+
const tmp = `${path}.tmp`;
|
|
3270
|
+
writeFileSync7(tmp, String(now));
|
|
3271
|
+
renameSync4(tmp, path);
|
|
3272
|
+
}
|
|
3273
|
+
function readHeartbeat(path) {
|
|
3274
|
+
try {
|
|
3275
|
+
const ms = Number(readFileSync7(path, "utf8").trim());
|
|
3276
|
+
return Number.isFinite(ms) ? ms : null;
|
|
3277
|
+
} catch {
|
|
3278
|
+
return null;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
function overmindAlive(path, now, staleMs) {
|
|
3282
|
+
const ts = readHeartbeat(path);
|
|
3283
|
+
if (ts == null) return false;
|
|
3284
|
+
return now - ts < staleMs;
|
|
3285
|
+
}
|
|
3286
|
+
var init_heartbeat = __esm({
|
|
3287
|
+
"src/heartbeat.ts"() {
|
|
3288
|
+
"use strict";
|
|
3289
|
+
}
|
|
3290
|
+
});
|
|
3291
|
+
|
|
3292
|
+
// src/logNoise.ts
|
|
3293
|
+
function timesWord(n) {
|
|
3294
|
+
const tens = n % 100;
|
|
3295
|
+
if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
|
|
3296
|
+
return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
|
|
3297
|
+
}
|
|
3298
|
+
function makeRepeatFilter(opts = {}) {
|
|
3299
|
+
const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
|
|
3300
|
+
const streaks = /* @__PURE__ */ new Map();
|
|
3301
|
+
return {
|
|
3302
|
+
say(key, message, now) {
|
|
3303
|
+
const prev = streaks.get(key);
|
|
3304
|
+
if (!prev || prev.message !== message) {
|
|
3305
|
+
const tail2 = prev && prev.hidden > 0 ? ` (\u0434\u043E \u044D\u0442\u043E\u0433\u043E \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 \u043C\u043E\u043B\u0447\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u043B\u043E\u0441\u044C \u0435\u0449\u0451 ${prev.hidden} ${timesWord(prev.hidden)})` : "";
|
|
3306
|
+
streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
|
|
3307
|
+
return `${message}${tail2}`;
|
|
3308
|
+
}
|
|
3309
|
+
prev.hidden += 1;
|
|
3310
|
+
if (now - prev.saidAt < remindAfterMs) return null;
|
|
3311
|
+
const hidden = prev.hidden;
|
|
3312
|
+
const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
|
|
3313
|
+
prev.saidAt = now;
|
|
3314
|
+
prev.hidden = 0;
|
|
3315
|
+
return `${message} \u2014 \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 ${hidden} ${timesWord(hidden)} \u043F\u043E\u0434\u0440\u044F\u0434, \u0442\u044F\u043D\u0435\u0442\u0441\u044F ${minutes} \u043C\u0438\u043D`;
|
|
3316
|
+
},
|
|
3317
|
+
forget(key) {
|
|
3318
|
+
streaks.delete(key);
|
|
3319
|
+
}
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
var DEFAULT_REMIND_MS;
|
|
3323
|
+
var init_logNoise = __esm({
|
|
3324
|
+
"src/logNoise.ts"() {
|
|
3325
|
+
"use strict";
|
|
3326
|
+
DEFAULT_REMIND_MS = 10 * 6e4;
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3190
3330
|
// src/procList.ts
|
|
3191
3331
|
function parsePsRows(out) {
|
|
3192
3332
|
const rows = [];
|
|
@@ -4836,78 +4976,6 @@ var init_oauth = __esm({
|
|
|
4836
4976
|
}
|
|
4837
4977
|
});
|
|
4838
4978
|
|
|
4839
|
-
// src/toolFailure.ts
|
|
4840
|
-
function trimDetail(s) {
|
|
4841
|
-
const t = s.trim();
|
|
4842
|
-
return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
|
|
4843
|
-
}
|
|
4844
|
-
function classifyToolFailure(raw) {
|
|
4845
|
-
if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
|
|
4846
|
-
if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
|
|
4847
|
-
if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
|
|
4848
|
-
return TOOL_FAILURE.TOOL_ERROR;
|
|
4849
|
-
}
|
|
4850
|
-
function classifyThrown(e) {
|
|
4851
|
-
const { kind, detail } = explainLinkFailure(e);
|
|
4852
|
-
const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
|
|
4853
|
-
// «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
|
|
4854
|
-
// и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
|
|
4855
|
-
classifyToolFailure(detail)
|
|
4856
|
-
);
|
|
4857
|
-
return { code, detail };
|
|
4858
|
-
}
|
|
4859
|
-
function describeToolFailure(f) {
|
|
4860
|
-
const text = TOOL_FAILURE_TEXT[f.code];
|
|
4861
|
-
const detail = trimDetail(f.detail);
|
|
4862
|
-
return detail ? `${text} (${detail})` : text;
|
|
4863
|
-
}
|
|
4864
|
-
var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
|
|
4865
|
-
var init_toolFailure = __esm({
|
|
4866
|
-
"src/toolFailure.ts"() {
|
|
4867
|
-
"use strict";
|
|
4868
|
-
init_linkFailure();
|
|
4869
|
-
TOOL_FAILURE = {
|
|
4870
|
-
/** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
|
|
4871
|
-
NOT_FOUND: "not_found",
|
|
4872
|
-
/** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
|
|
4873
|
-
FORBIDDEN: "forbidden",
|
|
4874
|
-
/** Токен роя не состоит в воркспейсе, которым искали. */
|
|
4875
|
-
WORKSPACE_MISMATCH: "workspace_mismatch",
|
|
4876
|
-
/** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
|
|
4877
|
-
UNREACHABLE: "unreachable",
|
|
4878
|
-
/** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
|
|
4879
|
-
UNAUTHORIZED: "unauthorized",
|
|
4880
|
-
/** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
|
|
4881
|
-
TOOL_ERROR: "tool_error",
|
|
4882
|
-
/** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
|
|
4883
|
-
EMPTY: "empty"
|
|
4884
|
-
};
|
|
4885
|
-
TOOL_FAILURE_TEXT = {
|
|
4886
|
-
[TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
|
|
4887
|
-
[TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
|
|
4888
|
-
[TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
|
|
4889
|
-
[TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
|
|
4890
|
-
[TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
|
|
4891
|
-
[TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
|
|
4892
|
-
[TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
|
|
4893
|
-
};
|
|
4894
|
-
TOOL_FAILURE_RETRIABLE = {
|
|
4895
|
-
[TOOL_FAILURE.NOT_FOUND]: false,
|
|
4896
|
-
[TOOL_FAILURE.FORBIDDEN]: false,
|
|
4897
|
-
[TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
|
|
4898
|
-
[TOOL_FAILURE.UNREACHABLE]: true,
|
|
4899
|
-
// Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
|
|
4900
|
-
[TOOL_FAILURE.UNAUTHORIZED]: false,
|
|
4901
|
-
[TOOL_FAILURE.TOOL_ERROR]: false,
|
|
4902
|
-
[TOOL_FAILURE.EMPTY]: false
|
|
4903
|
-
};
|
|
4904
|
-
FAILURE_DETAIL_MAX = 300;
|
|
4905
|
-
NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
|
|
4906
|
-
FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
|
|
4907
|
-
MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
|
|
4908
|
-
}
|
|
4909
|
-
});
|
|
4910
|
-
|
|
4911
4979
|
// src/authState.ts
|
|
4912
4980
|
function noteTokenRejected(nowMs = Date.now()) {
|
|
4913
4981
|
if (!denial) {
|
|
@@ -6621,7 +6689,7 @@ init_config();
|
|
|
6621
6689
|
init_resources();
|
|
6622
6690
|
init_names2();
|
|
6623
6691
|
init_storm();
|
|
6624
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as
|
|
6692
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
6625
6693
|
import { dirname as dirname2 } from "path";
|
|
6626
6694
|
import { randomUUID } from "crypto";
|
|
6627
6695
|
|
|
@@ -6673,13 +6741,16 @@ function renderPendingRulesBlock(rules) {
|
|
|
6673
6741
|
].join("\n");
|
|
6674
6742
|
}
|
|
6675
6743
|
|
|
6744
|
+
// src/swarmClient.ts
|
|
6745
|
+
init_toolFailure();
|
|
6746
|
+
|
|
6676
6747
|
// src/logTail.ts
|
|
6677
6748
|
import { closeSync, existsSync as existsSync6, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
|
|
6678
|
-
import { join as
|
|
6749
|
+
import { join as join9 } from "path";
|
|
6679
6750
|
|
|
6680
6751
|
// src/runLogDir.ts
|
|
6681
6752
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync4, renameSync as renameSync3, statSync as statSync3 } from "fs";
|
|
6682
|
-
import { join as
|
|
6753
|
+
import { join as join8 } from "path";
|
|
6683
6754
|
var LEGACY_RUN_LOGS_SUBDIR = "legacy";
|
|
6684
6755
|
function runLogDirName(runLogKey) {
|
|
6685
6756
|
const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
|
|
@@ -6687,7 +6758,7 @@ function runLogDirName(runLogKey) {
|
|
|
6687
6758
|
return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
|
|
6688
6759
|
}
|
|
6689
6760
|
function runLogDirPath(runLogsDir, runLogKey) {
|
|
6690
|
-
return
|
|
6761
|
+
return join8(runLogsDir, runLogDirName(runLogKey));
|
|
6691
6762
|
}
|
|
6692
6763
|
function findRunLogDir(runLogsDir, keys) {
|
|
6693
6764
|
for (const key of keys) {
|
|
@@ -6702,7 +6773,7 @@ function runLogDirForWrite(runLogsDir, keys) {
|
|
|
6702
6773
|
return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
|
|
6703
6774
|
}
|
|
6704
6775
|
function sweepLegacyRunLogs(runLogsDir) {
|
|
6705
|
-
const legacyDir =
|
|
6776
|
+
const legacyDir = join8(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
|
|
6706
6777
|
let entries;
|
|
6707
6778
|
try {
|
|
6708
6779
|
entries = readdirSync4(runLogsDir);
|
|
@@ -6713,12 +6784,12 @@ function sweepLegacyRunLogs(runLogsDir) {
|
|
|
6713
6784
|
let failed = 0;
|
|
6714
6785
|
for (const name of entries) {
|
|
6715
6786
|
if (!/^\d+$/.test(name)) continue;
|
|
6716
|
-
const from =
|
|
6787
|
+
const from = join8(runLogsDir, name);
|
|
6717
6788
|
try {
|
|
6718
6789
|
if (!statSync3(from).isDirectory()) continue;
|
|
6719
6790
|
mkdirSync5(legacyDir, { recursive: true });
|
|
6720
|
-
let to =
|
|
6721
|
-
for (let n = 2; existsSync5(to) && n < 100; n++) to =
|
|
6791
|
+
let to = join8(legacyDir, name);
|
|
6792
|
+
for (let n = 2; existsSync5(to) && n < 100; n++) to = join8(legacyDir, `${name}-${n}`);
|
|
6722
6793
|
renameSync3(from, to);
|
|
6723
6794
|
moved++;
|
|
6724
6795
|
} catch {
|
|
@@ -6784,8 +6855,8 @@ function latestRunLaunch(runLogsDir, runLogKeys) {
|
|
|
6784
6855
|
}
|
|
6785
6856
|
if (indexes.size === 0) return null;
|
|
6786
6857
|
const index = Math.max(...indexes);
|
|
6787
|
-
const out = [
|
|
6788
|
-
const err = existsSync6(
|
|
6858
|
+
const out = [join9(dir, `${index}.json`), join9(dir, `${index}.json.done`)].find((p) => existsSync6(p)) ?? null;
|
|
6859
|
+
const err = existsSync6(join9(dir, `${index}.err.log`)) ? join9(dir, `${index}.err.log`) : null;
|
|
6789
6860
|
return { index, total: indexes.size, out, err };
|
|
6790
6861
|
}
|
|
6791
6862
|
function runLogTail(runLogsDir, runLogKeys, opts) {
|
|
@@ -6872,6 +6943,14 @@ function toolErrorText(res) {
|
|
|
6872
6943
|
if (!text) return "\u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433 \u0432\u044B\u0437\u043E\u0432 \u0431\u0435\u0437 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u044F";
|
|
6873
6944
|
return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
|
|
6874
6945
|
}
|
|
6946
|
+
var DEPS_UNRETRIABLE_CODES = /* @__PURE__ */ new Set([
|
|
6947
|
+
TOOL_FAILURE.FORBIDDEN,
|
|
6948
|
+
TOOL_FAILURE.UNAUTHORIZED,
|
|
6949
|
+
TOOL_FAILURE.WORKSPACE_MISMATCH
|
|
6950
|
+
]);
|
|
6951
|
+
function depsFailureRetriable(code) {
|
|
6952
|
+
return !DEPS_UNRETRIABLE_CODES.has(code);
|
|
6953
|
+
}
|
|
6875
6954
|
function runRef(runKey) {
|
|
6876
6955
|
return { __runRef: runKey };
|
|
6877
6956
|
}
|
|
@@ -6968,6 +7047,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
6968
7047
|
// дальше удвоением до потолка TTL: устойчиво отвергающий прод (старее демона, скоуп
|
|
6969
7048
|
// токена) не должен получать вопрос каждые четыре секунды до конца своей жизни.
|
|
6970
7049
|
static DEPS_RETRY_BASE_MS = 5e3;
|
|
7050
|
+
// TASWM-517: сколько раз спрашиваем НЕСАМОЛЕЧАЩИЙСЯ отказ, прежде чем перестать вовсе.
|
|
7051
|
+
// `agent_forbidden` («тул недоступен аккаунту-агенту роя») повтором не чинится: 26.08
|
|
7052
|
+
// два прогона получили его по 53 раза каждый и остались в очереди навсегда. Три захода
|
|
7053
|
+
// при backoff 5с/10с — это ~15 секунд: запас на случай, если классификатор ошибся и
|
|
7054
|
+
// отказ на деле транзиентный, но не час молчаливого простоя. По исчерпании прогон
|
|
7055
|
+
// роняет гейт спавна (DEPS_UNRETRIABLE_ATTEMPTS читается и там).
|
|
7056
|
+
static DEPS_UNRETRIABLE_ATTEMPTS = 3;
|
|
6971
7057
|
// TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
|
|
6972
7058
|
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
6973
7059
|
// каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
|
|
@@ -7039,13 +7125,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
7039
7125
|
recover() {
|
|
7040
7126
|
if (existsSync7(this.mapPath)) {
|
|
7041
7127
|
try {
|
|
7042
|
-
const obj = JSON.parse(
|
|
7128
|
+
const obj = JSON.parse(readFileSync6(this.mapPath, "utf8"));
|
|
7043
7129
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
7044
7130
|
} catch {
|
|
7045
7131
|
}
|
|
7046
7132
|
}
|
|
7047
7133
|
if (existsSync7(this.outboxPath)) {
|
|
7048
|
-
const txt =
|
|
7134
|
+
const txt = readFileSync6(this.outboxPath, "utf8");
|
|
7049
7135
|
for (const line of txt.split("\n")) {
|
|
7050
7136
|
const s = line.trim();
|
|
7051
7137
|
if (!s) continue;
|
|
@@ -7068,7 +7154,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7068
7154
|
if (!existsSync7(this.stormPath)) return;
|
|
7069
7155
|
let txt;
|
|
7070
7156
|
try {
|
|
7071
|
-
txt =
|
|
7157
|
+
txt = readFileSync6(this.stormPath, "utf8");
|
|
7072
7158
|
} catch {
|
|
7073
7159
|
return;
|
|
7074
7160
|
}
|
|
@@ -7573,11 +7659,12 @@ var SwarmClient = class _SwarmClient {
|
|
|
7573
7659
|
res = await this.call("swarm_deps_status", { refs: ask });
|
|
7574
7660
|
} catch (e) {
|
|
7575
7661
|
for (const ref of ask) this.depsAskedAt.delete(ref);
|
|
7576
|
-
this.noteDepsUnanswered(ask, nowMs, String(e));
|
|
7662
|
+
this.noteDepsUnanswered(ask, nowMs, String(e), depsFailureRetriable(classifyThrown(e).code));
|
|
7577
7663
|
return;
|
|
7578
7664
|
}
|
|
7579
7665
|
if (res?.isError) {
|
|
7580
|
-
|
|
7666
|
+
const text = toolErrorText(res);
|
|
7667
|
+
this.noteDepsUnanswered(ask, nowMs, text, depsFailureRetriable(classifyToolFailure(text)));
|
|
7581
7668
|
return;
|
|
7582
7669
|
}
|
|
7583
7670
|
const deps = res.structuredContent?.deps ?? [];
|
|
@@ -7617,6 +7704,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7617
7704
|
depsRetryDelay(ref) {
|
|
7618
7705
|
const fail = this.depsUnanswered.get(ref);
|
|
7619
7706
|
if (fail) {
|
|
7707
|
+
if (!fail.retriable && fail.attempts >= _SwarmClient.DEPS_UNRETRIABLE_ATTEMPTS) return _SwarmClient.DEPS_TTL_MS;
|
|
7620
7708
|
const backoff = _SwarmClient.DEPS_RETRY_BASE_MS * 2 ** Math.min(fail.attempts - 1, 10);
|
|
7621
7709
|
return Math.min(backoff, _SwarmClient.DEPS_TTL_MS);
|
|
7622
7710
|
}
|
|
@@ -7625,17 +7713,19 @@ var SwarmClient = class _SwarmClient {
|
|
|
7625
7713
|
return _SwarmClient.DEPS_TTL_MS;
|
|
7626
7714
|
}
|
|
7627
7715
|
/** Запомнить, что прод по этим ссылкам не ответил (см. depsUnanswered / TASWM-486). */
|
|
7628
|
-
noteDepsUnanswered(refs, nowMs, error) {
|
|
7716
|
+
noteDepsUnanswered(refs, nowMs, error, retriable = true) {
|
|
7629
7717
|
for (const ref of refs) {
|
|
7630
7718
|
const prev = this.depsUnanswered.get(ref);
|
|
7719
|
+
const sameKind = prev != null && prev.retriable === retriable;
|
|
7631
7720
|
this.depsUnanswered.set(ref, {
|
|
7632
7721
|
ref,
|
|
7633
7722
|
slug: this.depSlug.get(ref) ?? ref,
|
|
7634
7723
|
// `since` — начало ЭПИЗОДА слепоты, а не последняя попытка: именно возраст эпизода
|
|
7635
7724
|
// отвечает на вопрос «сколько прогон уже стоит вслепую».
|
|
7636
7725
|
since: prev?.since ?? nowMs,
|
|
7637
|
-
attempts:
|
|
7638
|
-
error
|
|
7726
|
+
attempts: sameKind ? prev.attempts + 1 : 1,
|
|
7727
|
+
error,
|
|
7728
|
+
retriable
|
|
7639
7729
|
});
|
|
7640
7730
|
}
|
|
7641
7731
|
}
|
|
@@ -9179,13 +9269,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
9179
9269
|
}
|
|
9180
9270
|
persistPending() {
|
|
9181
9271
|
try {
|
|
9182
|
-
|
|
9272
|
+
writeFileSync6(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
9183
9273
|
} catch {
|
|
9184
9274
|
}
|
|
9185
9275
|
}
|
|
9186
9276
|
persistMap() {
|
|
9187
9277
|
try {
|
|
9188
|
-
|
|
9278
|
+
writeFileSync6(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
9189
9279
|
} catch {
|
|
9190
9280
|
}
|
|
9191
9281
|
}
|
|
@@ -9255,7 +9345,7 @@ function tickPulse(health, now, intervalMs) {
|
|
|
9255
9345
|
}
|
|
9256
9346
|
|
|
9257
9347
|
// src/logRotate.ts
|
|
9258
|
-
import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as
|
|
9348
|
+
import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
|
|
9259
9349
|
var realIo = {
|
|
9260
9350
|
size: (path) => {
|
|
9261
9351
|
try {
|
|
@@ -9267,7 +9357,7 @@ var realIo = {
|
|
|
9267
9357
|
copy: copyFileSync2,
|
|
9268
9358
|
rename: renameSync5,
|
|
9269
9359
|
truncate: (path) => truncateSync(path, 0),
|
|
9270
|
-
unlink:
|
|
9360
|
+
unlink: unlinkSync3,
|
|
9271
9361
|
exists: existsSync8
|
|
9272
9362
|
};
|
|
9273
9363
|
var realOwnerIo = {
|
|
@@ -11124,25 +11214,76 @@ var UNRETRIABLE_PARK_MARK = "\u043F\u043E\u0432\u0442\u043E\u0440 \u043D\u0435 \
|
|
|
11124
11214
|
function isUnretriableParked(run) {
|
|
11125
11215
|
return isPreStartFailure(run) && (run.stop_reason ?? "").includes(UNRETRIABLE_PARK_MARK);
|
|
11126
11216
|
}
|
|
11217
|
+
var PARK_APPLIED_MARK = "\u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u043D\u044F\u0442\u0430 \u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u043E\u044F";
|
|
11218
|
+
var PARK_APPLIED_RE = new RegExp(`${PARK_APPLIED_MARK} \\(([^,)]+)(?:, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E #(\\d+))?\\)`);
|
|
11219
|
+
function withParkApplied(reason, at, upToRunId) {
|
|
11220
|
+
if (PARK_APPLIED_RE.test(reason)) return reason;
|
|
11221
|
+
const upTo = upToRunId != null ? `, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E #${upToRunId}` : "";
|
|
11222
|
+
return `${reason} \u2014 ${PARK_APPLIED_MARK} (${at}${upTo})`;
|
|
11223
|
+
}
|
|
11224
|
+
function parkApplied(run) {
|
|
11225
|
+
if (!run || !isParkApplied(run)) return null;
|
|
11226
|
+
const m = PARK_APPLIED_RE.exec(run.stop_reason ?? "");
|
|
11227
|
+
if (!m) return null;
|
|
11228
|
+
const ms = m[1] ? stampMs(m[1]) : Number.NaN;
|
|
11229
|
+
return {
|
|
11230
|
+
at: Number.isFinite(ms) ? ms : null,
|
|
11231
|
+
upToRunId: m[2] ? Number(m[2]) : null
|
|
11232
|
+
};
|
|
11233
|
+
}
|
|
11234
|
+
function lastRunIdFor(runs, taskSlug) {
|
|
11235
|
+
return ownRuns(runs, taskSlug).reduce((max, r) => Math.max(max, r.id), 0);
|
|
11236
|
+
}
|
|
11237
|
+
function isParkApplied(run) {
|
|
11238
|
+
return isPreStartFailure(run) && (run.stop_reason ?? "").includes(PARK_APPLIED_MARK);
|
|
11239
|
+
}
|
|
11240
|
+
var PARK_CAUSE_PREFIX = "\u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ";
|
|
11241
|
+
function parkedCause(run) {
|
|
11242
|
+
const at = (run?.stop_reason ?? "").indexOf(PARK_CAUSE_PREFIX);
|
|
11243
|
+
if (at < 0) return null;
|
|
11244
|
+
const cause = run.stop_reason.slice(at + PARK_CAUSE_PREFIX.length).replace(new RegExp(`\\s*\u2014\\s*${PARK_APPLIED_MARK} \\([^)]*\\)\\s*$`), "").trim();
|
|
11245
|
+
return cause || null;
|
|
11246
|
+
}
|
|
11127
11247
|
var SPAWN_ATTEMPT_WINDOW_MS = 60 * 6e4;
|
|
11248
|
+
function ownRuns(runs, taskSlug) {
|
|
11249
|
+
return runs.filter(
|
|
11250
|
+
(r) => r.task_slug === taskSlug && r.reviews_run_id == null && r.resolves_run_id == null && r.verifies_run_id == null
|
|
11251
|
+
);
|
|
11252
|
+
}
|
|
11253
|
+
function newestFirst(a, b) {
|
|
11254
|
+
const at = a.created_at ? stampMs(a.created_at) : Number.NaN;
|
|
11255
|
+
const bt = b.created_at ? stampMs(b.created_at) : Number.NaN;
|
|
11256
|
+
if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return bt - at;
|
|
11257
|
+
return b.id - a.id;
|
|
11258
|
+
}
|
|
11128
11259
|
function preStartAttempts(runs, taskSlug, opts = {}) {
|
|
11129
11260
|
const windowMs = opts.windowMs ?? SPAWN_ATTEMPT_WINDOW_MS;
|
|
11130
11261
|
const now = opts.now ?? Date.now();
|
|
11131
|
-
const own = runs
|
|
11262
|
+
const own = ownRuns(runs, taskSlug).sort(newestFirst);
|
|
11132
11263
|
let n = 0;
|
|
11264
|
+
let newestCreatedAt = Number.NEGATIVE_INFINITY;
|
|
11265
|
+
let newestSeenId = Number.NEGATIVE_INFINITY;
|
|
11133
11266
|
for (const run of own) {
|
|
11267
|
+
const applied = parkApplied(run);
|
|
11268
|
+
if (applied) {
|
|
11269
|
+
const returned = applied.upToRunId != null ? newestSeenId > applied.upToRunId : applied.at != null && newestCreatedAt > applied.at;
|
|
11270
|
+
if (returned) break;
|
|
11271
|
+
}
|
|
11272
|
+
newestSeenId = Math.max(newestSeenId, run.id);
|
|
11273
|
+
const startedAt = run.created_at ? stampMs(run.created_at) : Number.NaN;
|
|
11274
|
+
if (Number.isFinite(startedAt)) newestCreatedAt = Math.max(newestCreatedAt, startedAt);
|
|
11134
11275
|
if (run.status === "queued" || run.status === "running") continue;
|
|
11135
11276
|
if (isSpawnParked(run) && run.status !== "failed") continue;
|
|
11136
11277
|
if (!isPreStartFailure(run)) break;
|
|
11137
|
-
|
|
11138
|
-
if (Number.isFinite(
|
|
11278
|
+
if (Number.isFinite(startedAt) && now - startedAt >= windowMs) break;
|
|
11279
|
+
if (opts.since != null && Number.isFinite(startedAt) && startedAt < opts.since) break;
|
|
11139
11280
|
n += 1;
|
|
11140
11281
|
if (isUnretriableParked(run)) return Math.max(n, PRE_START_ATTEMPT_LIMIT);
|
|
11141
11282
|
}
|
|
11142
11283
|
return n;
|
|
11143
11284
|
}
|
|
11144
11285
|
function lastPreStartAttempt(runs, taskSlug) {
|
|
11145
|
-
return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort(
|
|
11286
|
+
return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort(newestFirst)[0] ?? null;
|
|
11146
11287
|
}
|
|
11147
11288
|
function unresolvedRepoReason(repo, served) {
|
|
11148
11289
|
const raw = repo.trim();
|
|
@@ -11325,6 +11466,17 @@ async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
|
|
|
11325
11466
|
return null;
|
|
11326
11467
|
}
|
|
11327
11468
|
}
|
|
11469
|
+
async function openPrsForTask(slug, repoDir, run = realRepoRunner) {
|
|
11470
|
+
if (!slug || !repoDir) return null;
|
|
11471
|
+
try {
|
|
11472
|
+
const out = await run(["pr", "list", "--state", "open", "--json", "url,headRefName,title", "--limit", "50"], repoDir);
|
|
11473
|
+
const arr = JSON.parse(out);
|
|
11474
|
+
if (!Array.isArray(arr)) return null;
|
|
11475
|
+
return arr.filter((p) => typeof p.url === "string" && prBelongsToTask(slug, { headRefName: p.headRefName, title: p.title })).map((p) => p.url);
|
|
11476
|
+
} catch {
|
|
11477
|
+
return null;
|
|
11478
|
+
}
|
|
11479
|
+
}
|
|
11328
11480
|
async function closePR(prUrl, run = realRunner) {
|
|
11329
11481
|
await run(["pr", "close", prUrl, "--comment", "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 (swarm)"]);
|
|
11330
11482
|
}
|
|
@@ -14074,7 +14226,7 @@ import { serve as serve2 } from "@hono/node-server";
|
|
|
14074
14226
|
// package.json
|
|
14075
14227
|
var package_default = {
|
|
14076
14228
|
name: "@taskless-app/swarm",
|
|
14077
|
-
version: "0.6.
|
|
14229
|
+
version: "0.6.28",
|
|
14078
14230
|
type: "module",
|
|
14079
14231
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
14080
14232
|
keywords: [
|
|
@@ -14125,7 +14277,7 @@ var package_default = {
|
|
|
14125
14277
|
execa: "^10.0.0",
|
|
14126
14278
|
zod: "3.25.76"
|
|
14127
14279
|
},
|
|
14128
|
-
gitHead: "
|
|
14280
|
+
gitHead: "62910fc63850764f4562b7ec3bd65b44d3557050"
|
|
14129
14281
|
};
|
|
14130
14282
|
|
|
14131
14283
|
// src/version.ts
|
|
@@ -16625,6 +16777,7 @@ var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
|
|
|
16625
16777
|
var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
|
|
16626
16778
|
var EXITED_NO_DONE_REASON = "\u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441\u0430\u043C\u0430, done() \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043D";
|
|
16627
16779
|
var ABORTED_STOP_REASON = "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C, done() \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043D";
|
|
16780
|
+
var SUBMITTED_EXIT_TEXT = "\u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C, \u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430 \u0443\u0436\u0435 \u0441\u0434\u0430\u043D\u0430 (PR \u043E\u0442\u043A\u0440\u044B\u0442, done() \u043F\u0440\u0438\u043D\u044F\u0442) \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u0432 \u0440\u0435\u0432\u044C\u044E, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D";
|
|
16628
16781
|
var EXIT_WORK_PUSHED = "\u0432\u0435\u0442\u043A\u0430 \u0435\u0441\u0442\u044C \u043D\u0430 origin \u2014 \u0435\u0441\u0442\u044C \u0447\u0442\u043E \u0441\u043F\u0430\u0441\u0430\u0442\u044C, PR \u043D\u0435 \u043E\u0442\u043A\u0440\u044B\u0442";
|
|
16629
16782
|
var EXIT_WORK_LOST = "\u0432\u0435\u0442\u043A\u0438 \u043D\u0430 origin \u043D\u0435\u0442 \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E";
|
|
16630
16783
|
var EXIT_WORK_UNKNOWN = "\u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C origin \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u2014 \u0446\u0435\u043B\u0430 \u043B\u0438 \u0440\u0430\u0431\u043E\u0442\u0430, \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E";
|
|
@@ -16757,7 +16910,7 @@ function lastCiCheckMs(db, runId) {
|
|
|
16757
16910
|
if (!evs.length) return null;
|
|
16758
16911
|
return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
|
|
16759
16912
|
}
|
|
16760
|
-
function reviewPhaseInputFor(deps, run) {
|
|
16913
|
+
function reviewPhaseInputFor(deps, run, projectWantsVerify = true) {
|
|
16761
16914
|
const events = deps.db.events(run.id);
|
|
16762
16915
|
let ciRed = false;
|
|
16763
16916
|
let ciFailing = [];
|
|
@@ -16782,20 +16935,35 @@ function reviewPhaseInputFor(deps, run) {
|
|
|
16782
16935
|
const attempts = deps.db.reviewerAttemptCountFor(run.id);
|
|
16783
16936
|
const lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
|
|
16784
16937
|
const reviewerDead = !reviewer && run.review_verdict == null && attempts >= REVIEW_SPAWN_LIMIT && (lastAttempt?.status === "failed" || lastAttempt?.status === "stalled" || lastAttempt?.status === "stopped");
|
|
16938
|
+
const verifyExpected = deps.spawnVerifier != null && projectWantsVerify;
|
|
16939
|
+
const verifierDead = verifyExpected && run.verify_verdict == null && deps.db.verifierCountFor(run.id) === 0 && deps.db.verifierAttemptCountFor(run.id) >= VERIFY_SPAWN_LIMIT;
|
|
16785
16940
|
return {
|
|
16786
16941
|
reviewVerdict: run.review_verdict,
|
|
16787
16942
|
ciRed,
|
|
16788
16943
|
ciFailing,
|
|
16789
16944
|
ciProgress,
|
|
16790
16945
|
reviewerName: reviewer?.name ?? null,
|
|
16791
|
-
reviewerDead
|
|
16946
|
+
reviewerDead,
|
|
16947
|
+
verifyVerdict: run.verify_verdict,
|
|
16948
|
+
verifyExpected,
|
|
16949
|
+
verifierDead
|
|
16792
16950
|
};
|
|
16793
16951
|
}
|
|
16794
|
-
function refreshReviewPhases(deps) {
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16952
|
+
async function refreshReviewPhases(deps) {
|
|
16953
|
+
const runs = deps.db.listRuns("review").filter((r) => r.reviews_run_id == null && r.resolves_run_id == null && !!r.pr_url);
|
|
16954
|
+
const wantsChecks = /* @__PURE__ */ new Map();
|
|
16955
|
+
const needsAnswer = (r) => deps.spawnVerifier != null && r.review_verdict === "approve" && r.verify_verdict == null;
|
|
16956
|
+
for (const run of runs) {
|
|
16957
|
+
if (!needsAnswer(run) || wantsChecks.has(run.repo)) continue;
|
|
16958
|
+
try {
|
|
16959
|
+
wantsChecks.set(run.repo, await projectWantsChecks(deps, run));
|
|
16960
|
+
} catch (e) {
|
|
16961
|
+
console.warn(`[overmind] autoReview \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (${run.repo}) \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D \u2014 \u0444\u0430\u0437\u0430 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:`, e);
|
|
16962
|
+
wantsChecks.set(run.repo, true);
|
|
16963
|
+
}
|
|
16964
|
+
}
|
|
16965
|
+
for (const run of runs) {
|
|
16966
|
+
const phase = computeReviewPhase(reviewPhaseInputFor(deps, run, wantsChecks.get(run.repo) ?? true));
|
|
16799
16967
|
if (run.review_phase !== phase.label) deps.db.updateRun(run.id, { review_phase: phase.label });
|
|
16800
16968
|
}
|
|
16801
16969
|
}
|
|
@@ -16897,7 +17065,14 @@ async function retireOutdatedReviewers(deps, run) {
|
|
|
16897
17065
|
narrate(deps, `${droneLabel(run)}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u0440\u043E\u0448\u043B\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0435\u0449\u0451 \u0436\u0438\u0432 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0435\u0433\u043E, \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u043F\u043E\u0432\u0435\u0440\u0445 \u043D\u0435 \u0434\u0435\u0440\u0436\u0443.`);
|
|
16898
17066
|
}
|
|
16899
17067
|
}
|
|
16900
|
-
async function finalizeMergedReview(deps, run) {
|
|
17068
|
+
async function finalizeMergedReview(deps, run, opts) {
|
|
17069
|
+
if (!opts?.workChecked) {
|
|
17070
|
+
const work = await taskWorkState(deps, run.task_slug, run.repo || "", run.pr_url ?? null);
|
|
17071
|
+
if (work.kind === "open") {
|
|
17072
|
+
noteOpenPrHold(deps, run, work);
|
|
17073
|
+
return false;
|
|
17074
|
+
}
|
|
17075
|
+
}
|
|
16901
17076
|
await deps.finishTask(run.task_slug, run.workspace_id);
|
|
16902
17077
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
16903
17078
|
});
|
|
@@ -16905,6 +17080,7 @@ async function finalizeMergedReview(deps, run) {
|
|
|
16905
17080
|
deps.db.updateRun(run.id, { status: "done" });
|
|
16906
17081
|
deps.db.addEvent(run.id, "done");
|
|
16907
17082
|
await closeReviewersOnDeadPr(deps, run.id, "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
|
|
17083
|
+
return true;
|
|
16908
17084
|
}
|
|
16909
17085
|
async function finalizeClosedPrReview(deps, run, reason) {
|
|
16910
17086
|
await closeReviewersOnDeadPr(deps, run.id, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E");
|
|
@@ -16918,7 +17094,7 @@ async function prGateForReviewer(deps, run, prUrl, when) {
|
|
|
16918
17094
|
const st = await deps.prStatus(prUrl).catch(() => null);
|
|
16919
17095
|
switch (prLiveness(st)) {
|
|
16920
17096
|
case "merged":
|
|
16921
|
-
await finalizeMergedReview(deps, run);
|
|
17097
|
+
if (!await finalizeMergedReview(deps, run)) return { kind: "wait" };
|
|
16922
17098
|
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, ${when} \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
|
|
16923
17099
|
return { kind: "finalized" };
|
|
16924
17100
|
case "closed":
|
|
@@ -16949,6 +17125,16 @@ function doneMergedPrForSlug(db, slug) {
|
|
|
16949
17125
|
);
|
|
16950
17126
|
return done?.pr_url ?? null;
|
|
16951
17127
|
}
|
|
17128
|
+
var OPEN_PR_HOLD_MARK = "\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u043E\u0441\u0442\u0430\u043B\u0438\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 PR";
|
|
17129
|
+
async function taskWorkState(deps, slug, repoDir, candidate) {
|
|
17130
|
+
const openPrs = deps.openPrsForTask ? await deps.openPrsForTask(slug, repoDir).catch(() => null) : null;
|
|
17131
|
+
return decideTaskWork({ mergedCandidate: candidate, openPrs });
|
|
17132
|
+
}
|
|
17133
|
+
function noteOpenPrHold(deps, run, v) {
|
|
17134
|
+
if (deps.db.events(run.id).some((e) => (e.text ?? "").includes(OPEN_PR_HOLD_MARK))) return;
|
|
17135
|
+
deps.db.addEvent(run.id, "step", formatOpenPrHold(v));
|
|
17136
|
+
narrate(deps, `${droneLabel(run)} \u2014 \u0443 ${run.task_slug} \u0435\u0449\u0451 \u043E\u0442\u043A\u0440\u044B\u0442 PR \u0441 \u043D\u0435\u0432\u043B\u0438\u0442\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 (${v.prs.join(", ")}), \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E.`);
|
|
17137
|
+
}
|
|
16952
17138
|
async function closeDuplicateTaskRun(deps, run, mergedUrl) {
|
|
16953
17139
|
if (run.pid != null && deps.kill && deps.isAlive(run.pid)) {
|
|
16954
17140
|
try {
|
|
@@ -16958,7 +17144,7 @@ async function closeDuplicateTaskRun(deps, run, mergedUrl) {
|
|
|
16958
17144
|
}
|
|
16959
17145
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
16960
17146
|
});
|
|
16961
|
-
const reason =
|
|
17147
|
+
const reason = formatMergedDuplicateReason(mergedUrl);
|
|
16962
17148
|
deps.db.updateRun(run.id, { status: "superseded", stop_reason: reason });
|
|
16963
17149
|
deps.db.addEvent(run.id, "superseded", reason);
|
|
16964
17150
|
}
|
|
@@ -16990,10 +17176,16 @@ async function closeMergedTaskRuns(deps) {
|
|
|
16990
17176
|
mergedUrl = await deps.mergedPrForBranch(slug, group[0].repo || "").catch(() => null);
|
|
16991
17177
|
}
|
|
16992
17178
|
if (!mergedUrl) continue;
|
|
17179
|
+
const work = await taskWorkState(deps, slug, group[0].repo || "", mergedUrl);
|
|
17180
|
+
if (work.kind === "open") {
|
|
17181
|
+
for (const run of nonTerminal) noteOpenPrHold(deps, run, work);
|
|
17182
|
+
continue;
|
|
17183
|
+
}
|
|
17184
|
+
if (work.kind === "merged") mergedUrl = work.pr;
|
|
16993
17185
|
const canonical = withPr && withPr.pr_url === mergedUrl && !isTerminalStatus(withPr.status) ? withPr : null;
|
|
16994
17186
|
for (const run of nonTerminal) {
|
|
16995
17187
|
if (canonical && run.id === canonical.id) {
|
|
16996
|
-
await finalizeMergedReview(deps, run);
|
|
17188
|
+
await finalizeMergedReview(deps, run, { workChecked: true });
|
|
16997
17189
|
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430 (\u0434\u0443\u0431\u043B\u044C-\u0440\u0430\u043D\u044B \u0440\u043E\u044F, TASWM-206).`);
|
|
16998
17190
|
} else {
|
|
16999
17191
|
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
@@ -17592,6 +17784,16 @@ async function wakeRun(deps, runId) {
|
|
|
17592
17784
|
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` };
|
|
17593
17785
|
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` };
|
|
17594
17786
|
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` };
|
|
17787
|
+
const authorSideStopped = deps.db.events(run.id).some(
|
|
17788
|
+
(e) => [RESOLVE_ESCALATION_MARK, CI_WAKE_ESCALATION_MARK, VERIFY_ESCALATION_MARK].some(
|
|
17789
|
+
(m) => (e.text ?? "").includes(m)
|
|
17790
|
+
)
|
|
17791
|
+
);
|
|
17792
|
+
const changesEscalated = deps.db.events(run.id).some((e) => (e.text ?? "").includes(`${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run.review_cycle ?? 0})`));
|
|
17793
|
+
const stoppedForHuman = deliberateReviewStop(deps.db, run) != null || changesEscalated || authorSideStopped;
|
|
17794
|
+
if (run.status === "review" && run.pr_url && !needsFixIteration(run) && !stoppedForHuman) {
|
|
17795
|
+
return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0441\u0434\u0430\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E (${run.pr_url}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E; \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0440\u043E\u0439 \u043F\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0443` };
|
|
17796
|
+
}
|
|
17595
17797
|
const worktreeExists = deps.worktreeExists ?? existsSync18;
|
|
17596
17798
|
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
17597
17799
|
const wt = run.worktree_path;
|
|
@@ -18194,17 +18396,31 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
18194
18396
|
// причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
|
|
18195
18397
|
});
|
|
18196
18398
|
}
|
|
18197
|
-
const spentAttempts =
|
|
18399
|
+
const spentAttempts = attemptsFor(deps, run.task_slug);
|
|
18198
18400
|
if (spentAttempts >= PRE_START_ATTEMPT_LIMIT) {
|
|
18199
18401
|
const last = lastPreStartAttempt(deps.db.listRuns(), run.task_slug);
|
|
18200
|
-
const
|
|
18402
|
+
const cause = parkedCause(last);
|
|
18403
|
+
const tail2 = cause ? ` \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${cause}` : "";
|
|
18404
|
+
const why = (last && isUnretriableParked(last) ? `${SPAWN_PARK_MARK}: \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043E\u0442\u043A\u0430\u0437\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-485). \u0427\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C \u2014 \u0432 \u043F\u0440\u043E\u0433\u043E\u043D\u0435 #${last.id}` : `${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)`) + tail2;
|
|
18201
18405
|
deps.db.updateRun(run.id, { status: "stopped", stop_reason: why });
|
|
18202
18406
|
deps.db.addEvent(run.id, "step", why);
|
|
18407
|
+
if (deps.parkTask && last && !isParkApplied(last)) {
|
|
18408
|
+
const ok = await deps.parkTask(run, last.stop_reason ?? why, { comment: false }).catch(() => false);
|
|
18409
|
+
if (ok)
|
|
18410
|
+
deps.db.updateRun(last.id, {
|
|
18411
|
+
// Водораздел включает и ЭТОТ, только что закрытый гейтом прогон: он появился до
|
|
18412
|
+
// подтверждения, и принимать его за ручной возврат нельзя (code-review PR #984).
|
|
18413
|
+
stop_reason: withParkApplied(last.stop_reason ?? why, stamp(), lastRunIdFor(deps.db.listRuns(), run.task_slug))
|
|
18414
|
+
});
|
|
18415
|
+
}
|
|
18203
18416
|
const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), `pre-start-exhausted:${run.task_slug}`, why, GATE_LOG_HEARTBEAT_TICKS);
|
|
18204
18417
|
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}`}`);
|
|
18205
18418
|
return "closed";
|
|
18206
18419
|
}
|
|
18207
|
-
const
|
|
18420
|
+
const mergedCandidate = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
18421
|
+
const work = mergedCandidate ? await taskWorkState(deps, run.task_slug, run.repo || "", mergedCandidate) : null;
|
|
18422
|
+
if (work?.kind === "open") noteOpenPrHold(deps, run, work);
|
|
18423
|
+
const mergedUrl = work?.kind === "merged" ? work.pr : null;
|
|
18208
18424
|
if (mergedUrl) {
|
|
18209
18425
|
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
18210
18426
|
await deps.finishTask(run.task_slug, run.workspace_id).catch(() => {
|
|
@@ -18217,6 +18433,15 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
18217
18433
|
if (unmet.length) {
|
|
18218
18434
|
const lost = unmet.filter((d) => d.outcome === "unknown");
|
|
18219
18435
|
const blind = deps.db.depsUnconfirmed?.(run.depends_on) ?? [];
|
|
18436
|
+
const stuck = blind.find((d) => !d.retriable && d.attempts >= SwarmClient.DEPS_UNRETRIABLE_ATTEMPTS);
|
|
18437
|
+
if (stuck) {
|
|
18438
|
+
return await failBeforeStart(
|
|
18439
|
+
deps,
|
|
18440
|
+
run,
|
|
18441
|
+
`\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C ${stuck.slug} \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C: \u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 swarm_deps_status \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u044D\u0442\u043E \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F (${stuck.attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A), \u043E\u0442\u0432\u0435\u0442 \u2014 ${stuck.error}). \u0412\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043D\u0435 \u0436\u0434\u0443 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 (TASWM-517)`,
|
|
18442
|
+
{ retriable: false }
|
|
18443
|
+
);
|
|
18444
|
+
}
|
|
18220
18445
|
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)` : blind.length ? `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 ${blind.map((d) => d.slug).join(", ")}, \u043D\u043E \u041F\u0420\u041E\u0414 \u0415\u0401 \u041D\u0415 \u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u0415\u0422: ${blind[0].attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) swarm_deps_status, \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u2014 ${blind[0].error}. \u041F\u043E\u043A\u0430 \u043F\u0440\u043E\u0434 \u043C\u043E\u043B\u0447\u0438\u0442, \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438 \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442 (TASWM-486)` : `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`;
|
|
18221
18446
|
return deferSpawn(deps, run, why);
|
|
18222
18447
|
}
|
|
@@ -18299,10 +18524,13 @@ function spawnRetryOpts(e) {
|
|
|
18299
18524
|
if (!(e instanceof TaskBodyUnavailableError)) return {};
|
|
18300
18525
|
return { retriable: TOOL_FAILURE_RETRIABLE[e.code] };
|
|
18301
18526
|
}
|
|
18527
|
+
function attemptsFor(deps, taskSlug) {
|
|
18528
|
+
return preStartAttempts(deps.db.listRuns(), taskSlug, deps.startedAt != null ? { since: deps.startedAt } : {});
|
|
18529
|
+
}
|
|
18302
18530
|
async function failBeforeStart(deps, run, reason, opts = {}) {
|
|
18303
18531
|
deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
|
|
18304
18532
|
deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
|
|
18305
|
-
const attempts =
|
|
18533
|
+
const attempts = attemptsFor(deps, run.task_slug);
|
|
18306
18534
|
if (opts.retriable !== false && attempts < PRE_START_ATTEMPT_LIMIT) {
|
|
18307
18535
|
narrate(deps, `${droneLabel(run)} \u2014 ${PRE_START_MARK} (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempts} \u0438\u0437 ${PRE_START_ATTEMPT_LIMIT}): ${reason}`);
|
|
18308
18536
|
return "failed";
|
|
@@ -18313,7 +18541,13 @@ async function failBeforeStart(deps, run, reason, opts = {}) {
|
|
|
18313
18541
|
narrate(deps, `${run.task_slug}: ${parked}`);
|
|
18314
18542
|
if (deps.parkTask) {
|
|
18315
18543
|
const ok = await deps.parkTask(run, parked).catch(() => false);
|
|
18316
|
-
if (
|
|
18544
|
+
if (ok)
|
|
18545
|
+
deps.db.updateRun(run.id, {
|
|
18546
|
+
// Водораздел (code-review PR #984) — самый большой id прогона задачи ПРЯМО СЕЙЧАС:
|
|
18547
|
+
// всё, что появится после него, заведено уже по возвращённой в очередь задаче.
|
|
18548
|
+
stop_reason: withParkApplied(parked, stamp(), lastRunIdFor(deps.db.listRuns(), run.task_slug))
|
|
18549
|
+
});
|
|
18550
|
+
else {
|
|
18317
18551
|
deps.db.addEvent(
|
|
18318
18552
|
run.id,
|
|
18319
18553
|
"step",
|
|
@@ -18983,8 +19217,7 @@ async function tickOnce(deps) {
|
|
|
18983
19217
|
const st = await deps.prStatus(prUrl);
|
|
18984
19218
|
run = syncBranchFromPr(deps.db, run, st.headBranch);
|
|
18985
19219
|
if (st.merged) {
|
|
18986
|
-
await finalizeMergedReview(deps, run);
|
|
18987
|
-
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
|
|
19220
|
+
if (await finalizeMergedReview(deps, run)) narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
|
|
18988
19221
|
continue;
|
|
18989
19222
|
}
|
|
18990
19223
|
if (prLiveness(st) === "closed") {
|
|
@@ -19128,7 +19361,7 @@ async function tickOnce(deps) {
|
|
|
19128
19361
|
relinkUnboundReviewers(deps);
|
|
19129
19362
|
await closeOrphanedReviewers(deps);
|
|
19130
19363
|
await spawnVerifiersForOpenPrs(deps, stormActive);
|
|
19131
|
-
refreshReviewPhases(deps);
|
|
19364
|
+
await refreshReviewPhases(deps);
|
|
19132
19365
|
let liveChildPpids;
|
|
19133
19366
|
try {
|
|
19134
19367
|
liveChildPpids = deps.livePpids?.() ?? null;
|
|
@@ -19218,6 +19451,12 @@ async function tickOnce(deps) {
|
|
|
19218
19451
|
}
|
|
19219
19452
|
}
|
|
19220
19453
|
if (!alive && (outcome === "graceful" || outcome === "aborted") && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.verifies_run_id == null && run.kind !== "hero") {
|
|
19454
|
+
if (!wasAborted(outcome) && run.pr_url) {
|
|
19455
|
+
deps.db.updateRun(run.id, { status: "review", stop_reason: null });
|
|
19456
|
+
deps.db.addEvent(run.id, "step", `${SUBMITTED_EXIT_TEXT}: ${run.pr_url}`);
|
|
19457
|
+
narrate(deps, `${droneLabel(run)} \u0432\u044B\u0448\u0435\u043B, \u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430 \u0443\u0436\u0435 \u0441\u0434\u0430\u043D\u0430 (${run.pr_url}) \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u0432 \u0440\u0435\u0432\u044C\u044E, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
|
|
19458
|
+
continue;
|
|
19459
|
+
}
|
|
19221
19460
|
const aborted = wasAborted(outcome);
|
|
19222
19461
|
const abort = aborted ? deps.exitAbort?.(run) ?? null : null;
|
|
19223
19462
|
const what = aborted ? abortReason(abort) : EXITED_NO_DONE_REASON;
|
|
@@ -20484,6 +20723,9 @@ async function runDaemon(opts) {
|
|
|
20484
20723
|
// Смерженный PR ветки-слага (TASWM-206) — тот же фолбэк на дефолт-репо роя, что и
|
|
20485
20724
|
// openPrsForBranch выше; реальный gh, логика закрытия дублей — в tickOnce (тестируется).
|
|
20486
20725
|
mergedPrForBranch: (branch, repoDir) => mergedPrForBranch(branch, repoDir || opts.repo),
|
|
20726
|
+
// ВСЕ открытые PR задачи (TASWM-515) — тот же фолбэк на дефолт-репо роя. Реальный gh;
|
|
20727
|
+
// и отбор «этот PR — задачи», и решение «закончена ли работа» чистые (swarm-core).
|
|
20728
|
+
openPrsForTask: (slug, repoDir) => openPrsForTask(slug, repoDir || opts.repo),
|
|
20487
20729
|
// Закрыта ли задача в Taskless (TASWM-137 п.г): death-detect гасит зомби-прогон, чья
|
|
20488
20730
|
// задача уже completed/cancelled/archived, вместо респавна. null — Taskless недоступен
|
|
20489
20731
|
// (не гасим). Без tl (нет токена) ключ не задаём — шаг инертен, старое поведение.
|
|
@@ -20499,17 +20741,22 @@ async function runDaemon(opts) {
|
|
|
20499
20741
|
// задачу), а не кодом возврата: клиент глушит отказ сервера внутрь (safeCall), и по
|
|
20500
20742
|
// возврату «отложил» было бы неотличимо от «сказал, но не доехало» — то самое молчание,
|
|
20501
20743
|
// из-за которого 17.08 круг и крутился.
|
|
20502
|
-
parkTask: async (run, reason) => {
|
|
20744
|
+
parkTask: async (run, reason, opts2) => {
|
|
20503
20745
|
const client = tlFor(run.workspace_id);
|
|
20504
20746
|
if (!client) return false;
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
|
|
20747
|
+
if (opts2?.comment !== false)
|
|
20748
|
+
await client.addComment(
|
|
20749
|
+
run.task_slug,
|
|
20750
|
+
// TASWM-514: советуем ИМЕННО возврат в очередь — теперь этот рычаг работает
|
|
20751
|
+
// (возврат в «не начато» обнуляет счётчик попыток). И причина стоит в первой
|
|
20752
|
+
// строке (её несёт `reason`, см. parkReason): человек, читающий пометку через
|
|
20753
|
+
// час, должен из неё понять, ЧТО чинить, а не раскапывать записи прогонов.
|
|
20754
|
+
`${reason}
|
|
20508
20755
|
|
|
20509
|
-
\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. \
|
|
20510
|
-
|
|
20511
|
-
|
|
20512
|
-
|
|
20756
|
+
\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. \u0423\u0441\u0442\u0440\u0430\u043D\u0438 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u0438 \u0432\u0435\u0440\u043D\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0432 \xAB\u043D\u0435 \u043D\u0430\u0447\u0430\u0442\u043E\xBB \u2014 \u0440\u043E\u0439 \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442 \u0435\u0451 \u0441\u043D\u043E\u0432\u0430.`,
|
|
20757
|
+
{ swarmKind: "report", swarmDroneName: run.name }
|
|
20758
|
+
).catch(() => {
|
|
20759
|
+
});
|
|
20513
20760
|
await client.startTask(run.task_slug).catch(() => {
|
|
20514
20761
|
});
|
|
20515
20762
|
const after = await client.getTask(run.task_slug).catch(() => null);
|
|
@@ -20723,6 +20970,9 @@ async function runDaemon(opts) {
|
|
|
20723
20970
|
nudge
|
|
20724
20971
|
),
|
|
20725
20972
|
worktreeExists: (p) => existsSync18(p),
|
|
20973
|
+
// TASWM-514: с этого момента считаем попытки спавна — счётчик живёт ровно одну жизнь
|
|
20974
|
+
// процесса демона и рестарт не переживает (см. preStartAttempts).
|
|
20975
|
+
startedAt: Date.now(),
|
|
20726
20976
|
hasTranscript: executorHasSession,
|
|
20727
20977
|
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
20728
20978
|
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|
|
@@ -21184,6 +21434,7 @@ export {
|
|
|
21184
21434
|
GATE_LOG_HEARTBEAT_TICKS,
|
|
21185
21435
|
HEADLESS_BLOCK,
|
|
21186
21436
|
HERO_CONTINUE_NUDGE,
|
|
21437
|
+
OPEN_PR_HOLD_MARK,
|
|
21187
21438
|
OVERMIND_ENTRY,
|
|
21188
21439
|
RESOLVE_ESCALATION_MARK,
|
|
21189
21440
|
RESOLVE_LIMIT,
|
|
@@ -21201,6 +21452,8 @@ export {
|
|
|
21201
21452
|
STALL_FLOOR_MS,
|
|
21202
21453
|
STALL_MS,
|
|
21203
21454
|
STORM_DEAD_LIMIT,
|
|
21455
|
+
SUBMITTED_EXIT_TEXT,
|
|
21456
|
+
VERIFY_ESCALATION_MARK,
|
|
21204
21457
|
VERIFY_SPAWN_LIMIT,
|
|
21205
21458
|
WATCHDOG_PARK_REASON,
|
|
21206
21459
|
abortReason,
|