@taskless-app/swarm 0.6.12 → 0.6.14

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.
Files changed (3) hide show
  1. package/dist/cli.js +137 -31
  2. package/dist/overmind.js +190 -40
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -452,6 +452,40 @@ var init_mergeGate = __esm({
452
452
  }
453
453
  });
454
454
 
455
+ // ../swarm-core/src/modelPolicy.ts
456
+ function findProvider(id) {
457
+ const key = typeof id === "string" ? id.trim().toLowerCase() : "";
458
+ return MODEL_PROVIDERS.find((p) => p.id === key) ?? null;
459
+ }
460
+ function parseModelRef(ref) {
461
+ const s = typeof ref === "string" ? ref.trim() : "";
462
+ if (!s) return null;
463
+ const slash = s.indexOf("/");
464
+ if (slash < 0) return { providerId: DEFAULT_PROVIDER_ID, model: s };
465
+ const providerId = s.slice(0, slash).trim().toLowerCase();
466
+ const model = s.slice(slash + 1).trim();
467
+ if (!model) return null;
468
+ return { providerId: providerId || DEFAULT_PROVIDER_ID, model };
469
+ }
470
+ function defaultModelFor(role, providerId = null) {
471
+ const provider = findProvider(providerId) ?? findProvider(DEFAULT_PROVIDER_ID);
472
+ return provider.roles[role];
473
+ }
474
+ var MODEL_PROVIDERS, DEFAULT_PROVIDER_ID;
475
+ var init_modelPolicy = __esm({
476
+ "../swarm-core/src/modelPolicy.ts"() {
477
+ "use strict";
478
+ MODEL_PROVIDERS = [
479
+ {
480
+ id: "anthropic",
481
+ label: "Anthropic",
482
+ roles: { executor: "claude-opus-5", review: "claude-sonnet-5" }
483
+ }
484
+ ];
485
+ DEFAULT_PROVIDER_ID = MODEL_PROVIDERS[0].id;
486
+ }
487
+ });
488
+
455
489
  // ../swarm-core/src/status.ts
456
490
  function statusMeta(status) {
457
491
  return STATUS_META[status] ?? FALLBACK;
@@ -687,6 +721,13 @@ var init_baseBranch = __esm({
687
721
  });
688
722
 
689
723
  // ../swarm-core/src/hosts.ts
724
+ function hostSignInHint(webUrl) {
725
+ const url = (webUrl ?? "").trim();
726
+ return url ? `\u0420\u043E\u0439 \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D. \u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 ${url} \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u0440\u043E\u044F \u0438 \u0432\u043E\u0439\u0434\u0438\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 Taskless` : "\u0420\u043E\u0439 \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D. \u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0443 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u0440\u043E\u044F \u0438 \u0432\u043E\u0439\u0434\u0438\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 Taskless";
727
+ }
728
+ function hostNotSignedIn(host) {
729
+ return host.signedIn === false;
730
+ }
690
731
  function normalizeHostRef(value) {
691
732
  if (value == null) return null;
692
733
  const trimmed = value.trim();
@@ -720,6 +761,10 @@ function groupByHost(items, ref, opts = {}) {
720
761
  offline: false,
721
762
  paused: unknown ? false : info?.paused === true,
722
763
  pauseReason: unknown ? null : info?.pauseReason ?? null,
764
+ // Поле ставим, ТОЛЬКО если машина о входе сказала: undefined здесь — «не знаем»,
765
+ // и превращать молчание в отказ значило бы объявить невойденным весь парк машин
766
+ // со старым демоном.
767
+ ...unknown || info?.signedIn == null ? {} : { signedIn: info.signedIn },
723
768
  runs: []
724
769
  };
725
770
  byKey.set(key, g);
@@ -752,6 +797,7 @@ function hostStateText(host, nowMs = Date.now(), staleMs2 = HOST_HEARTBEAT_STALE
752
797
  const at = Date.parse(host.lastSeenAt);
753
798
  if (Number.isNaN(at)) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
754
799
  if (!isHostOffline(host.lastSeenAt, nowMs, staleMs2)) {
800
+ if (hostNotSignedIn(host)) return hostSignInHint(host.webUrl);
755
801
  if (host.paused) return host.pauseReason ? `\u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 ${host.pauseReason}` : "\u043D\u0430 \u043F\u0430\u0443\u0437\u0435";
756
802
  if (host.storm) return host.stormReason ? `\u0448\u0442\u043E\u0440\u043C\u0438\u0442 \u2014 ${host.stormReason}` : "\u0448\u0442\u043E\u0440\u043C\u0438\u0442 \u2014 \u043D\u043E\u0432\u044B\u0435 \u0441\u0435\u0441\u0441\u0438\u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442";
757
803
  return "\u043D\u0430 \u0441\u0432\u044F\u0437\u0438";
@@ -873,6 +919,42 @@ var init_cost = __esm({
873
919
  }
874
920
  });
875
921
 
922
+ // ../swarm-core/src/modelPricing.ts
923
+ function findModelPricing(model, table = MODEL_PRICING) {
924
+ if (!model) return void 0;
925
+ const exact = table[model];
926
+ if (exact) return exact;
927
+ let bestKey = "";
928
+ for (const key of Object.keys(table)) {
929
+ if (model.startsWith(key) && key.length > bestKey.length) bestKey = key;
930
+ }
931
+ return bestKey ? table[bestKey] : void 0;
932
+ }
933
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
934
+ var init_modelPricing = __esm({
935
+ "../swarm-core/src/modelPricing.ts"() {
936
+ "use strict";
937
+ MODEL_PRICING = {
938
+ // Sonnet 5 — 2/10. Цена ПОСТОЯННАЯ: 18.08.2026 владелец подтвердил, что вводной
939
+ // она быть перестала. Даты годности у тарифа тут больше нет и заводить её заново не
940
+ // нужно: тариф со сроком означает, что в календарный день расчёт молча меняет цифру,
941
+ // и «почему прогон вчера стоил иначе» приходится выяснять по коду, а не по таблице.
942
+ // Меняется цена — меняется строка, одним видимым коммитом.
943
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
944
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
945
+ "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
946
+ // Прогоны и вызовы на 4.x остаются в истории — строки держим ради их стоимости.
947
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
948
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
949
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
950
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
951
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
952
+ };
953
+ CACHE_READ_FACTOR = 0.1;
954
+ CACHE_WRITE_5M_FACTOR = 1.25;
955
+ }
956
+ });
957
+
876
958
  // ../swarm-core/src/runGuards.ts
877
959
  function remainingRunBudgetUsd(maxCostUsd, spentUsd) {
878
960
  if (!(maxCostUsd > 0)) return null;
@@ -1316,6 +1398,7 @@ var init_src = __esm({
1316
1398
  init_reviewPhase();
1317
1399
  init_mergeGate();
1318
1400
  init_verifyVerdict();
1401
+ init_modelPolicy();
1319
1402
  init_status();
1320
1403
  init_deps();
1321
1404
  init_time();
@@ -1331,6 +1414,7 @@ var init_src = __esm({
1331
1414
  init_hostCommands();
1332
1415
  init_relief();
1333
1416
  init_cost();
1417
+ init_modelPricing();
1334
1418
  init_runGuards();
1335
1419
  init_codeZones();
1336
1420
  init_effort();
@@ -1428,18 +1512,12 @@ var init_swarmFiles = __esm({
1428
1512
  });
1429
1513
 
1430
1514
  // src/pricing.ts
1431
- function pricingAt(pricing, at) {
1432
- const intro = pricing.intro;
1433
- if (!intro) return pricing;
1434
- const day = at.toISOString().slice(0, 10);
1435
- return day <= intro.until ? { inputPerMTok: intro.inputPerMTok, outputPerMTok: intro.outputPerMTok } : pricing;
1436
- }
1437
- function costFromUsage(usage2, model, table = MODEL_PRICING, at = /* @__PURE__ */ new Date()) {
1438
- const entry = model ? table[model] : void 0;
1515
+ function costFromUsage(usage2, model, table = MODEL_PRICING) {
1516
+ const entry = findModelPricing(model, table);
1439
1517
  if (!entry) {
1440
1518
  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` };
1441
1519
  }
1442
- const pricing = pricingAt(entry, at);
1520
+ const pricing = entry;
1443
1521
  const input = usage2.input_tokens ?? 0;
1444
1522
  const output = usage2.output_tokens ?? 0;
1445
1523
  const cacheWrite = usage2.cache_creation_input_tokens ?? 0;
@@ -1447,24 +1525,10 @@ function costFromUsage(usage2, model, table = MODEL_PRICING, at = /* @__PURE__ *
1447
1525
  const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1448
1526
  return { costUsd };
1449
1527
  }
1450
- var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1451
1528
  var init_pricing = __esm({
1452
1529
  "src/pricing.ts"() {
1453
1530
  "use strict";
1454
- MODEL_PRICING = {
1455
- // Sonnet 5: вводные 2/10 по 31.08.2026 включительно, дальше базовые 3/15.
1456
- "claude-sonnet-5": {
1457
- inputPerMTok: 3,
1458
- outputPerMTok: 15,
1459
- intro: { inputPerMTok: 2, outputPerMTok: 10, until: "2026-08-31" }
1460
- },
1461
- "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1462
- // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1463
- "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1464
- "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1465
- };
1466
- CACHE_READ_FACTOR = 0.1;
1467
- CACHE_WRITE_5M_FACTOR = 1.25;
1531
+ init_src();
1468
1532
  }
1469
1533
  });
1470
1534
 
@@ -2103,7 +2167,7 @@ var init_package = __esm({
2103
2167
  "package.json"() {
2104
2168
  package_default = {
2105
2169
  name: "@taskless-app/swarm",
2106
- version: "0.6.12",
2170
+ version: "0.6.14",
2107
2171
  type: "module",
2108
2172
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
2109
2173
  keywords: [
@@ -2153,7 +2217,7 @@ var init_package = __esm({
2153
2217
  execa: "^10.0.0",
2154
2218
  zod: "3.25.76"
2155
2219
  },
2156
- gitHead: "43c844efdbea61a27c54edc17a111d658ce6e2b4"
2220
+ gitHead: "a3b11498dbed5db3642fe4fc83bcd2054efde055"
2157
2221
  };
2158
2222
  }
2159
2223
  });
@@ -2677,6 +2741,7 @@ function rowToRun(row) {
2677
2741
  area: row.area ?? null,
2678
2742
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
2679
2743
  model: row.model ?? null,
2744
+ review_model: row.review_model ?? null,
2680
2745
  // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
2681
2746
  // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
2682
2747
  // непонятное здесь означает «уровень не задан», а не сломанный тип.
@@ -2754,6 +2819,7 @@ CREATE TABLE IF NOT EXISTS runs (
2754
2819
  area TEXT,
2755
2820
  depends_on TEXT,
2756
2821
  model TEXT,
2822
+ review_model TEXT,
2757
2823
  effort TEXT,
2758
2824
  cost_usd REAL NOT NULL DEFAULT 0,
2759
2825
  tokens_in INTEGER NOT NULL DEFAULT 0,
@@ -2867,7 +2933,11 @@ CREATE TABLE IF NOT EXISTS imports (
2867
2933
  "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
2868
2934
  // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
2869
2935
  // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
2870
- "ALTER TABLE runs ADD COLUMN effort TEXT"
2936
+ "ALTER TABLE runs ADD COLUMN effort TEXT",
2937
+ // TASWM-451: чем проверять работу этого прогона — модель ревью, приехавшая с задачи.
2938
+ // Живёт на прогоне ИСПОЛНИТЕЛЯ: ревьюер и верификатор заводятся локально и читают её
2939
+ // оттуда, а не ходят за задачей в прод в момент спавна.
2940
+ "ALTER TABLE runs ADD COLUMN review_model TEXT"
2871
2941
  ]) {
2872
2942
  try {
2873
2943
  this.db.exec(alter);
@@ -2887,7 +2957,7 @@ CREATE TABLE IF NOT EXISTS imports (
2887
2957
  createRun(input) {
2888
2958
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
2889
2959
  const res = this.db.prepare(
2890
- "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
2960
+ "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
2891
2961
  ).run(
2892
2962
  input.task_slug,
2893
2963
  name,
@@ -2900,6 +2970,7 @@ CREATE TABLE IF NOT EXISTS imports (
2900
2970
  input.area ?? null,
2901
2971
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
2902
2972
  input.model ?? null,
2973
+ input.review_model ?? null,
2903
2974
  input.effort ?? null,
2904
2975
  // усилие рассуждения (TASWM-395); null → разрешит спавн
2905
2976
  input.decision ? JSON.stringify(input.decision) : null,
@@ -5390,6 +5461,7 @@ var SwarmClient = class _SwarmClient {
5390
5461
  if (patch.status === "running") out.executor = this.executorId;
5391
5462
  if ("area" in patch) out.area = patch.area ?? null;
5392
5463
  if ("model" in patch) out.model = patch.model ?? null;
5464
+ if ("review_model" in patch) out.reviewModel = patch.review_model ?? null;
5393
5465
  if ("effort" in patch) out.effort = patch.effort ?? null;
5394
5466
  if ("name" in patch) out.droneName = patch.name ?? null;
5395
5467
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
@@ -5463,6 +5535,10 @@ var SwarmClient = class _SwarmClient {
5463
5535
  area: input.area ?? null,
5464
5536
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
5465
5537
  model: input.model ?? null,
5538
+ // TASWM-451: чем проверять эту работу. У прогона исполнителя приезжает из задачи
5539
+ // (claim в проде), у локально заведённого ревьюера/верификатора — наследством от
5540
+ // проверяемого прогона, которое ставит spawnReviewer/spawnVerifier.
5541
+ review_model: input.review_model ?? null,
5466
5542
  // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
5467
5543
  // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
5468
5544
  effort: input.effort ?? null,
@@ -5521,6 +5597,8 @@ var SwarmClient = class _SwarmClient {
5521
5597
  status: "queued",
5522
5598
  area: d.area,
5523
5599
  model: d.model,
5600
+ review_model: d.review_model,
5601
+ // TASWM-451: чем проверять — едет рядом с моделью
5524
5602
  effort: d.effort,
5525
5603
  // усилие рассуждения (TASWM-395), если пинили на спавне
5526
5604
  name: d.name,
@@ -6648,7 +6726,7 @@ var SwarmClient = class _SwarmClient {
6648
6726
  *
6649
6727
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
6650
6728
  */
6651
- async heartbeat(daemonVersion, pause, capacity, resources, storm) {
6729
+ async heartbeat(daemonVersion, pause, capacity, resources, storm, signIn) {
6652
6730
  try {
6653
6731
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
6654
6732
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -6658,6 +6736,10 @@ var SwarmClient = class _SwarmClient {
6658
6736
  args.paused = !!pause?.paused;
6659
6737
  if (pause?.reason) args.pauseReason = pause.reason;
6660
6738
  }
6739
+ if (signIn) {
6740
+ args.signedIn = signIn.signedIn;
6741
+ if (signIn.webUrl) args.webUrl = signIn.webUrl;
6742
+ }
6661
6743
  if (storm !== void 0 && storm !== null) {
6662
6744
  args.storm = storm.active;
6663
6745
  if (storm.reason) args.stormReason = storm.reason;
@@ -6889,6 +6971,8 @@ var SwarmClient = class _SwarmClient {
6889
6971
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
6890
6972
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
6891
6973
  review_cycle: 0,
6974
+ // TASWM-451: модель ПРОВЕРКИ — прод-поле, заполняется ниже из ответа (как base_branch).
6975
+ review_model: null,
6892
6976
  task_title: null,
6893
6977
  task_desc: null,
6894
6978
  task_url: null,
@@ -6941,6 +7025,7 @@ var SwarmClient = class _SwarmClient {
6941
7025
  local.area = pr.area ?? null;
6942
7026
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
6943
7027
  local.model = pr.model ?? null;
7028
+ local.review_model = pr.reviewModel ?? null;
6944
7029
  local.effort = toEffortLevel(pr.effort);
6945
7030
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
6946
7031
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
@@ -9985,12 +10070,26 @@ function pick(v) {
9985
10070
  const s = typeof v === "string" ? v.trim() : "";
9986
10071
  return s.length > 0 ? s : null;
9987
10072
  }
10073
+ function isReviewKind(kind) {
10074
+ return kind === "reviewer" || kind === "verifier";
10075
+ }
10076
+ function modelRoleOf(kind) {
10077
+ return isReviewKind(kind) ? "review" : "executor";
10078
+ }
9988
10079
  function forKind(kind, swarm, norm) {
9989
10080
  if (!swarm) return null;
9990
- return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
10081
+ return isReviewKind(kind) ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
10082
+ }
10083
+ function resolveRunModelRef(run, swarm, env = process.env) {
10084
+ const configured = parseModelRef(
10085
+ pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL)
10086
+ );
10087
+ if (configured) return configured;
10088
+ const providerId = pick(env.SWARM_MODEL_PROVIDER) ?? DEFAULT_PROVIDER_ID;
10089
+ return { providerId, model: defaultModelFor(modelRoleOf(run.kind), providerId) };
9991
10090
  }
9992
10091
  function resolveRunModel(run, swarm, env = process.env) {
9993
- return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
10092
+ return resolveRunModelRef(run, swarm, env).model;
9994
10093
  }
9995
10094
  function resolveRunEffort(run, swarm, env = process.env) {
9996
10095
  return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
@@ -11922,6 +12021,13 @@ switch (cmd) {
11922
12021
  // должен внезапно поднимать дрона другим харнессом.
11923
12022
  executor: EXECUTOR,
11924
12023
  droneIsolation: DRONE_ISOLATION,
12024
+ // TASWM-450: ПОТОЛОК ТРАТ обязателен и здесь. Демон собирает лаунчер с
12025
+ // `maxCostUsd: () => lastMaxCostUsd`, а этот путь его не передавал вовсе — значит
12026
+ // разбуженный руками прогон уходил БЕЗ `--max-budget-usd` и мог жечь сколько угодно.
12027
+ // Живой настройки роя у CLI нет (демон не отвечает — иначе мы сюда не попали бы),
12028
+ // поэтому источник тот же, что и у остальных ~40 ручек: env машины, а за ним дефолт
12029
+ // ядра. Это ровно то, что делает `launchBudgetUsd(null, …)`.
12030
+ maxCostUsd: () => null,
11925
12031
  ...DRONE_CHANNEL_ENABLED ? {
11926
12032
  channel: {
11927
12033
  url: DRONE_CHANNEL_URL,
package/dist/overmind.js CHANGED
@@ -425,6 +425,40 @@ var init_mergeGate = __esm({
425
425
  }
426
426
  });
427
427
 
428
+ // ../swarm-core/src/modelPolicy.ts
429
+ function findProvider(id) {
430
+ const key = typeof id === "string" ? id.trim().toLowerCase() : "";
431
+ return MODEL_PROVIDERS.find((p) => p.id === key) ?? null;
432
+ }
433
+ function parseModelRef(ref) {
434
+ const s = typeof ref === "string" ? ref.trim() : "";
435
+ if (!s) return null;
436
+ const slash = s.indexOf("/");
437
+ if (slash < 0) return { providerId: DEFAULT_PROVIDER_ID, model: s };
438
+ const providerId = s.slice(0, slash).trim().toLowerCase();
439
+ const model = s.slice(slash + 1).trim();
440
+ if (!model) return null;
441
+ return { providerId: providerId || DEFAULT_PROVIDER_ID, model };
442
+ }
443
+ function defaultModelFor(role, providerId = null) {
444
+ const provider = findProvider(providerId) ?? findProvider(DEFAULT_PROVIDER_ID);
445
+ return provider.roles[role];
446
+ }
447
+ var MODEL_PROVIDERS, DEFAULT_PROVIDER_ID;
448
+ var init_modelPolicy = __esm({
449
+ "../swarm-core/src/modelPolicy.ts"() {
450
+ "use strict";
451
+ MODEL_PROVIDERS = [
452
+ {
453
+ id: "anthropic",
454
+ label: "Anthropic",
455
+ roles: { executor: "claude-opus-5", review: "claude-sonnet-5" }
456
+ }
457
+ ];
458
+ DEFAULT_PROVIDER_ID = MODEL_PROVIDERS[0].id;
459
+ }
460
+ });
461
+
428
462
  // ../swarm-core/src/status.ts
429
463
  function isAwaitingCi(status, stopReason) {
430
464
  return status === "paused" && stopReason === AWAITING_CI_REASON;
@@ -989,6 +1023,42 @@ var init_cost = __esm({
989
1023
  }
990
1024
  });
991
1025
 
1026
+ // ../swarm-core/src/modelPricing.ts
1027
+ function findModelPricing(model, table = MODEL_PRICING) {
1028
+ if (!model) return void 0;
1029
+ const exact = table[model];
1030
+ if (exact) return exact;
1031
+ let bestKey = "";
1032
+ for (const key of Object.keys(table)) {
1033
+ if (model.startsWith(key) && key.length > bestKey.length) bestKey = key;
1034
+ }
1035
+ return bestKey ? table[bestKey] : void 0;
1036
+ }
1037
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1038
+ var init_modelPricing = __esm({
1039
+ "../swarm-core/src/modelPricing.ts"() {
1040
+ "use strict";
1041
+ MODEL_PRICING = {
1042
+ // Sonnet 5 — 2/10. Цена ПОСТОЯННАЯ: 18.08.2026 владелец подтвердил, что вводной
1043
+ // она быть перестала. Даты годности у тарифа тут больше нет и заводить её заново не
1044
+ // нужно: тариф со сроком означает, что в календарный день расчёт молча меняет цифру,
1045
+ // и «почему прогон вчера стоил иначе» приходится выяснять по коду, а не по таблице.
1046
+ // Меняется цена — меняется строка, одним видимым коммитом.
1047
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1048
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1049
+ "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
1050
+ // Прогоны и вызовы на 4.x остаются в истории — строки держим ради их стоимости.
1051
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1052
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
1053
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
1054
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
1055
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1056
+ };
1057
+ CACHE_READ_FACTOR = 0.1;
1058
+ CACHE_WRITE_5M_FACTOR = 1.25;
1059
+ }
1060
+ });
1061
+
992
1062
  // ../swarm-core/src/runGuards.ts
993
1063
  function remainingRunBudgetUsd(maxCostUsd, spentUsd) {
994
1064
  if (!(maxCostUsd > 0)) return null;
@@ -1448,6 +1518,7 @@ var init_src = __esm({
1448
1518
  init_reviewPhase();
1449
1519
  init_mergeGate();
1450
1520
  init_verifyVerdict();
1521
+ init_modelPolicy();
1451
1522
  init_status();
1452
1523
  init_deps();
1453
1524
  init_time();
@@ -1463,6 +1534,7 @@ var init_src = __esm({
1463
1534
  init_hostCommands();
1464
1535
  init_relief();
1465
1536
  init_cost();
1537
+ init_modelPricing();
1466
1538
  init_runGuards();
1467
1539
  init_codeZones();
1468
1540
  init_effort();
@@ -1597,18 +1669,12 @@ var init_swarmFiles = __esm({
1597
1669
  });
1598
1670
 
1599
1671
  // src/pricing.ts
1600
- function pricingAt(pricing, at) {
1601
- const intro = pricing.intro;
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;
1672
+ function costFromUsage(usage, model, table = MODEL_PRICING) {
1673
+ const entry = findModelPricing(model, table);
1608
1674
  if (!entry) {
1609
1675
  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
1676
  }
1611
- const pricing = pricingAt(entry, at);
1677
+ const pricing = entry;
1612
1678
  const input = usage.input_tokens ?? 0;
1613
1679
  const output = usage.output_tokens ?? 0;
1614
1680
  const cacheWrite = usage.cache_creation_input_tokens ?? 0;
@@ -1616,24 +1682,10 @@ function costFromUsage(usage, model, table = MODEL_PRICING, at = /* @__PURE__ */
1616
1682
  const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1617
1683
  return { costUsd };
1618
1684
  }
1619
- var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1620
1685
  var init_pricing = __esm({
1621
1686
  "src/pricing.ts"() {
1622
1687
  "use strict";
1623
- MODEL_PRICING = {
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;
1688
+ init_src();
1637
1689
  }
1638
1690
  });
1639
1691
 
@@ -3500,6 +3552,7 @@ function bad(status, error) {
3500
3552
  function scopeOf(store, run) {
3501
3553
  const ids = /* @__PURE__ */ new Set([run.id]);
3502
3554
  if (run.reviews_run_id != null) ids.add(run.reviews_run_id);
3555
+ if (run.verifies_run_id != null) ids.add(run.verifies_run_id);
3503
3556
  if (run.kind === "hero") for (const r of store.squadRuns(run.id)) ids.add(r.id);
3504
3557
  return ids;
3505
3558
  }
@@ -3512,6 +3565,10 @@ function invalidPatchValue(field, v) {
3512
3565
  return typeof v === "string" && v in STATUS_META ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441: ${String(v)}`;
3513
3566
  case "review_verdict":
3514
3567
  return v === null || v === "approve" || v === "changes" ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442: ${String(v)}`;
3568
+ // TASWM-444: словарь исходов верификатора — один на весь продукт (swarm-core), поэтому
3569
+ // проверяем его же функцией, а не повторяем литералы третьим списком.
3570
+ case "verify_verdict":
3571
+ return v === null || isVerifyVerdict(v) ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430: ${String(v)}`;
3515
3572
  case "review_cycle":
3516
3573
  return typeof v === "number" && Number.isInteger(v) && v >= 0 ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0446\u0438\u043A\u043B \u0440\u0435\u0432\u044C\u044E: ${String(v)}`;
3517
3574
  case "pr_url":
@@ -3775,7 +3832,16 @@ var init_droneChannel = __esm({
3775
3832
  "use strict";
3776
3833
  init_src();
3777
3834
  LOCAL_CLI_KEY = "cli";
3778
- PATCHABLE = /* @__PURE__ */ new Set(["status", "pr_url", "review_verdict", "review_cycle"]);
3835
+ PATCHABLE = /* @__PURE__ */ new Set([
3836
+ "status",
3837
+ "pr_url",
3838
+ "review_verdict",
3839
+ // TASWM-444: вердикт верификатора — такая же сдача работы, как review_verdict у ревьюера,
3840
+ // и пишется он тем же путём на ПРОВЕРЯЕМЫЙ прогон. Без него роль verifier была немой:
3841
+ // вердикт выносился комментарием на PR, а `done` отбивался каналом.
3842
+ "verify_verdict",
3843
+ "review_cycle"
3844
+ ]);
3779
3845
  EVENT_KIND_SET = new Set(EVENT_KINDS);
3780
3846
  ChannelUnavailableError = class extends Error {
3781
3847
  constructor(downMs, message) {
@@ -4347,6 +4413,10 @@ function authStateLine(nowMs = Date.now()) {
4347
4413
  if (!denial) return null;
4348
4414
  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`;
4349
4415
  }
4416
+ function signedInToTaskless() {
4417
+ if (denial) return false;
4418
+ return Boolean(loadAgentToken()?.token);
4419
+ }
4350
4420
  function deniedSinceLine(nowMs) {
4351
4421
  const d = denial;
4352
4422
  if (!d) return "";
@@ -4910,6 +4980,7 @@ function rowToRun(row) {
4910
4980
  area: row.area ?? null,
4911
4981
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
4912
4982
  model: row.model ?? null,
4983
+ review_model: row.review_model ?? null,
4913
4984
  // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
4914
4985
  // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
4915
4986
  // непонятное здесь означает «уровень не задан», а не сломанный тип.
@@ -4987,6 +5058,7 @@ CREATE TABLE IF NOT EXISTS runs (
4987
5058
  area TEXT,
4988
5059
  depends_on TEXT,
4989
5060
  model TEXT,
5061
+ review_model TEXT,
4990
5062
  effort TEXT,
4991
5063
  cost_usd REAL NOT NULL DEFAULT 0,
4992
5064
  tokens_in INTEGER NOT NULL DEFAULT 0,
@@ -5100,7 +5172,11 @@ CREATE TABLE IF NOT EXISTS imports (
5100
5172
  "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
5101
5173
  // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
5102
5174
  // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
5103
- "ALTER TABLE runs ADD COLUMN effort TEXT"
5175
+ "ALTER TABLE runs ADD COLUMN effort TEXT",
5176
+ // TASWM-451: чем проверять работу этого прогона — модель ревью, приехавшая с задачи.
5177
+ // Живёт на прогоне ИСПОЛНИТЕЛЯ: ревьюер и верификатор заводятся локально и читают её
5178
+ // оттуда, а не ходят за задачей в прод в момент спавна.
5179
+ "ALTER TABLE runs ADD COLUMN review_model TEXT"
5104
5180
  ]) {
5105
5181
  try {
5106
5182
  this.db.exec(alter);
@@ -5120,7 +5196,7 @@ CREATE TABLE IF NOT EXISTS imports (
5120
5196
  createRun(input) {
5121
5197
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
5122
5198
  const res = this.db.prepare(
5123
- "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
5199
+ "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
5124
5200
  ).run(
5125
5201
  input.task_slug,
5126
5202
  name,
@@ -5133,6 +5209,7 @@ CREATE TABLE IF NOT EXISTS imports (
5133
5209
  input.area ?? null,
5134
5210
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
5135
5211
  input.model ?? null,
5212
+ input.review_model ?? null,
5136
5213
  input.effort ?? null,
5137
5214
  // усилие рассуждения (TASWM-395); null → разрешит спавн
5138
5215
  input.decision ? JSON.stringify(input.decision) : null,
@@ -6292,6 +6369,7 @@ var SwarmClient = class _SwarmClient {
6292
6369
  if (patch.status === "running") out.executor = this.executorId;
6293
6370
  if ("area" in patch) out.area = patch.area ?? null;
6294
6371
  if ("model" in patch) out.model = patch.model ?? null;
6372
+ if ("review_model" in patch) out.reviewModel = patch.review_model ?? null;
6295
6373
  if ("effort" in patch) out.effort = patch.effort ?? null;
6296
6374
  if ("name" in patch) out.droneName = patch.name ?? null;
6297
6375
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
@@ -6365,6 +6443,10 @@ var SwarmClient = class _SwarmClient {
6365
6443
  area: input.area ?? null,
6366
6444
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
6367
6445
  model: input.model ?? null,
6446
+ // TASWM-451: чем проверять эту работу. У прогона исполнителя приезжает из задачи
6447
+ // (claim в проде), у локально заведённого ревьюера/верификатора — наследством от
6448
+ // проверяемого прогона, которое ставит spawnReviewer/spawnVerifier.
6449
+ review_model: input.review_model ?? null,
6368
6450
  // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
6369
6451
  // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
6370
6452
  effort: input.effort ?? null,
@@ -6423,6 +6505,8 @@ var SwarmClient = class _SwarmClient {
6423
6505
  status: "queued",
6424
6506
  area: d.area,
6425
6507
  model: d.model,
6508
+ review_model: d.review_model,
6509
+ // TASWM-451: чем проверять — едет рядом с моделью
6426
6510
  effort: d.effort,
6427
6511
  // усилие рассуждения (TASWM-395), если пинили на спавне
6428
6512
  name: d.name,
@@ -7550,7 +7634,7 @@ var SwarmClient = class _SwarmClient {
7550
7634
  *
7551
7635
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
7552
7636
  */
7553
- async heartbeat(daemonVersion, pause, capacity, resources, storm) {
7637
+ async heartbeat(daemonVersion, pause, capacity, resources, storm, signIn) {
7554
7638
  try {
7555
7639
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
7556
7640
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -7560,6 +7644,10 @@ var SwarmClient = class _SwarmClient {
7560
7644
  args.paused = !!pause?.paused;
7561
7645
  if (pause?.reason) args.pauseReason = pause.reason;
7562
7646
  }
7647
+ if (signIn) {
7648
+ args.signedIn = signIn.signedIn;
7649
+ if (signIn.webUrl) args.webUrl = signIn.webUrl;
7650
+ }
7563
7651
  if (storm !== void 0 && storm !== null) {
7564
7652
  args.storm = storm.active;
7565
7653
  if (storm.reason) args.stormReason = storm.reason;
@@ -7791,6 +7879,8 @@ var SwarmClient = class _SwarmClient {
7791
7879
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
7792
7880
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
7793
7881
  review_cycle: 0,
7882
+ // TASWM-451: модель ПРОВЕРКИ — прод-поле, заполняется ниже из ответа (как base_branch).
7883
+ review_model: null,
7794
7884
  task_title: null,
7795
7885
  task_desc: null,
7796
7886
  task_url: null,
@@ -7843,6 +7933,7 @@ var SwarmClient = class _SwarmClient {
7843
7933
  local.area = pr.area ?? null;
7844
7934
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
7845
7935
  local.model = pr.model ?? null;
7936
+ local.review_model = pr.reviewModel ?? null;
7846
7937
  local.effort = toEffortLevel(pr.effort);
7847
7938
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
7848
7939
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
@@ -8881,12 +8972,26 @@ function pick(v) {
8881
8972
  const s = typeof v === "string" ? v.trim() : "";
8882
8973
  return s.length > 0 ? s : null;
8883
8974
  }
8975
+ function isReviewKind(kind) {
8976
+ return kind === "reviewer" || kind === "verifier";
8977
+ }
8978
+ function modelRoleOf(kind) {
8979
+ return isReviewKind(kind) ? "review" : "executor";
8980
+ }
8884
8981
  function forKind(kind, swarm, norm) {
8885
8982
  if (!swarm) return null;
8886
- return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
8983
+ return isReviewKind(kind) ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
8984
+ }
8985
+ function resolveRunModelRef(run, swarm, env = process.env) {
8986
+ const configured = parseModelRef(
8987
+ pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL)
8988
+ );
8989
+ if (configured) return configured;
8990
+ const providerId = pick(env.SWARM_MODEL_PROVIDER) ?? DEFAULT_PROVIDER_ID;
8991
+ return { providerId, model: defaultModelFor(modelRoleOf(run.kind), providerId) };
8887
8992
  }
8888
8993
  function resolveRunModel(run, swarm, env = process.env) {
8889
- return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8994
+ return resolveRunModelRef(run, swarm, env).model;
8890
8995
  }
8891
8996
  function resolveRunEffort(run, swarm, env = process.env) {
8892
8997
  return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
@@ -10108,11 +10213,16 @@ import { z } from "zod";
10108
10213
  init_src();
10109
10214
 
10110
10215
  // src/reviewCycle.ts
10216
+ init_src();
10111
10217
  function reviewTransitionPatch(run, prUrl) {
10112
- const isFixIteration = run.review_verdict === "changes";
10218
+ const isFixIteration = run.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run.verify_verdict));
10113
10219
  if (isFixIteration) {
10114
10220
  const cycle = (run.review_cycle ?? 0) + 1;
10115
- return { isFixIteration: true, cycle, patch: { status: "review", pr_url: prUrl, review_cycle: cycle, review_verdict: null } };
10221
+ return {
10222
+ isFixIteration: true,
10223
+ cycle,
10224
+ patch: { status: "review", pr_url: prUrl, review_cycle: cycle, review_verdict: null, verify_verdict: null }
10225
+ };
10116
10226
  }
10117
10227
  return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
10118
10228
  }
@@ -12187,7 +12297,7 @@ async function changedPaths(worktree, opts = {}) {
12187
12297
  // package.json
12188
12298
  var package_default = {
12189
12299
  name: "@taskless-app/swarm",
12190
- version: "0.6.12",
12300
+ version: "0.6.14",
12191
12301
  type: "module",
12192
12302
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12193
12303
  keywords: [
@@ -12237,7 +12347,7 @@ var package_default = {
12237
12347
  execa: "^10.0.0",
12238
12348
  zod: "3.25.76"
12239
12349
  },
12240
- gitHead: "43c844efdbea61a27c54edc17a111d658ce6e2b4"
12350
+ gitHead: "a3b11498dbed5db3642fe4fc83bcd2054efde055"
12241
12351
  };
12242
12352
 
12243
12353
  // src/version.ts
@@ -14974,7 +15084,14 @@ async function prGateForReviewer(deps, run, prUrl, when) {
14974
15084
  }
14975
15085
  }
14976
15086
  function doneMergedPrForSlug(db, slug) {
14977
- const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
15087
+ const done = db.listRuns("done").find(
15088
+ (r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null && // TASWM-446: и НЕ верификатор. «done = смержено» верно только для прогона, который PR
15089
+ // делал; верификатор же заканчивает в `done`, держа `pr_url` ПРОВЕРЯЕМОГО — открытого —
15090
+ // PR, и его завершение читалось как доказательство мержа. Дальше pre-spawn гейт закрывал
15091
+ // исполнителя как дубль: 17.08 так погибли пять живых прогонов подряд, а задача после
15092
+ // этого не бралась вовсе — следующий дрон умирал на том же месте.
15093
+ r.verifies_run_id == null
15094
+ );
14978
15095
  return done?.pr_url ?? null;
14979
15096
  }
14980
15097
  async function closeDuplicateTaskRun(deps, run, mergedUrl) {
@@ -14993,7 +15110,8 @@ async function closeDuplicateTaskRun(deps, run, mergedUrl) {
14993
15110
  async function closeMergedTaskRuns(deps) {
14994
15111
  const bySlug = /* @__PURE__ */ new Map();
14995
15112
  for (const run of deps.db.listRuns()) {
14996
- if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null) continue;
15113
+ if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null || run.verifies_run_id != null)
15114
+ continue;
14997
15115
  let group = bySlug.get(run.task_slug);
14998
15116
  if (!group) bySlug.set(run.task_slug, group = []);
14999
15117
  group.push(run);
@@ -15190,6 +15308,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
15190
15308
  "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.",
15191
15309
  `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.`,
15192
15310
  "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.",
15311
+ "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.",
15193
15312
  "\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.",
15194
15313
  "\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.",
15195
15314
  "\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."
@@ -16782,7 +16901,7 @@ async function tickOnce(deps) {
16782
16901
  } else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
16783
16902
  await handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap);
16784
16903
  }
16785
- if (run.review_verdict === "changes" && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
16904
+ if ((run.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run.verify_verdict))) && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
16786
16905
  continue;
16787
16906
  }
16788
16907
  const reviewerGatesOk = !st.conflicting && !st.isDraft && !!deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
@@ -17205,8 +17324,14 @@ async function replayDecision(deps, qid, chosen, by = "human") {
17205
17324
  repo: old.repo,
17206
17325
  area: old.area,
17207
17326
  decision: { question: q.text, options: q.options, chosen, prior: q.answer, priorState: old.status },
17208
- workspace_id: old.workspace_id
17327
+ workspace_id: old.workspace_id,
17209
17328
  // переигровка той же задачи — тот же воркспейс (TASWM-288)
17329
+ // TASWM-451: та же ЗАДАЧА — те же модели. Переигровка это продолжение той же работы, а не
17330
+ // новая: без переноса любой вопрос дрона оператору молча возвращал бы задачу на дефолт
17331
+ // роли, и выбор постановщика («эту делаем сильной моделью») терялся бы ровно там, где
17332
+ // работа оказалась сложной. Тот же довод, что у area и workspace_id строкой выше.
17333
+ model: old.model,
17334
+ review_model: old.review_model
17210
17335
  });
17211
17336
  deps.db.updateRun(old.id, { status: "superseded" });
17212
17337
  deps.db.supersede(old.id, fresh.id, by);
@@ -17234,8 +17359,13 @@ async function resolveConflict(deps, reviewRun) {
17234
17359
  area: reviewRun.area,
17235
17360
  // держим area-lock, пока чиним — не начинаем конфликтующее в зоне
17236
17361
  resolves_run_id: reviewRun.id,
17237
- workspace_id: reviewRun.workspace_id
17362
+ workspace_id: reviewRun.workspace_id,
17238
17363
  // та же задача, тот же воркспейс (TASWM-288)
17364
+ // TASWM-451: резолвер доделывает ТУ ЖЕ работу — разводит конфликт в её же ветке, и
17365
+ // модель ему нужна та же, что выбрали задаче. Модель проверки несём дальше, чтобы её не
17366
+ // потеряли проверяющие, которых поднимут уже по прогону резолвера.
17367
+ model: reviewRun.model,
17368
+ review_model: reviewRun.review_model
17239
17369
  });
17240
17370
  const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17241
17371
  try {
@@ -17282,6 +17412,11 @@ async function spawnReviewer(deps, reviewRun, files) {
17282
17412
  task_slug: reviewRun.task_slug,
17283
17413
  repo,
17284
17414
  kind: "reviewer",
17415
+ // TASWM-451: ЧЕМ проверять — решение постановщика, приехавшее с задачей на прогон
17416
+ // исполнителя. Пин, а не «спросим настройки на спавне»: ревьюер заводится локально, и
17417
+ // задача ему недоступна. Пусто в задаче → null → дефолт роли (модель проверки).
17418
+ model: reviewRun.review_model,
17419
+ review_model: reviewRun.review_model,
17285
17420
  reviews_run_id: reviewRun.id,
17286
17421
  review_cycle: reviewRun.review_cycle,
17287
17422
  // штамп цикла (авто-ревью v2, TASWM-162): reviewerCountFor текущего цикла
@@ -17354,6 +17489,11 @@ async function spawnVerifier(deps, verifiedRun, files) {
17354
17489
  task_slug: verifiedRun.task_slug,
17355
17490
  repo,
17356
17491
  kind: "verifier",
17492
+ // TASWM-451: та же модель проверки, что у ревьюера, и из того же источника — задачи.
17493
+ // Две проверки одного PR должны стоить одинаково, иначе одна из них незаметно уедет
17494
+ // на модель исполнителя (ровно это и стоило $45.94 на TAS-524).
17495
+ model: verifiedRun.review_model,
17496
+ review_model: verifiedRun.review_model,
17357
17497
  verifies_run_id: verifiedRun.id,
17358
17498
  review_cycle: verifiedRun.review_cycle,
17359
17499
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
@@ -17724,7 +17864,17 @@ async function runDaemon(opts) {
17724
17864
  control.state ?? null,
17725
17865
  lastOwnCapacity,
17726
17866
  hostResourcesForProd(),
17727
- { 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 }
17867
+ { 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 },
17868
+ // TASWM-393: ВХОД в Taskless — та же природа, что пауза и шторм: состояние
17869
+ // машины, о котором знает только она. Пульс идёт и без входа, поэтому «на
17870
+ // связи» без этого поля читалось как «возьмёт работу» (12.08 — шесть прогонов
17871
+ // в очереди у машины, которая снаружи выглядела здоровой). Адрес шлём
17872
+ // ФАКТИЧЕСКИЙ (web.port), а не зашитый: порт настраивается.
17873
+ // signedInToTaskless(), а НЕ !authStateLine(): отказ — это «токен есть, но его
17874
+ // отвергают», а 12.08 входа не делали вовсе, и по отказу машина числилась бы
17875
+ // вошедшей. Адрес — WEB_PORT (он же SWARM_WEB_PORT из окружения, на нём и
17876
+ // поднимается морда): зашитый 7420 отправил бы человека туда, где ничего нет.
17877
+ { signedIn: signedInToTaskless(), webUrl: `http://127.0.0.1:${WEB_PORT}` }
17728
17878
  );
17729
17879
  if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
17730
17880
  if (reply.commands.length) onHostCommands(reply.commands);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "type": "module",
5
5
  "description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "execa": "^10.0.0",
39
39
  "zod": "3.25.76"
40
40
  },
41
- "gitHead": "43c844efdbea61a27c54edc17a111d658ce6e2b4",
41
+ "gitHead": "a3b11498dbed5db3642fe4fc83bcd2054efde055",
42
42
  "scripts": {
43
43
  "overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
44
44
  "swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",