@taskless-app/swarm 0.6.13 → 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 +165 -37
  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.13",
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: "ea3913bf66c52d730c25ad62b7090621a5006ee7"
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
 
@@ -4361,6 +4413,10 @@ function authStateLine(nowMs = Date.now()) {
4361
4413
  if (!denial) return null;
4362
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`;
4363
4415
  }
4416
+ function signedInToTaskless() {
4417
+ if (denial) return false;
4418
+ return Boolean(loadAgentToken()?.token);
4419
+ }
4364
4420
  function deniedSinceLine(nowMs) {
4365
4421
  const d = denial;
4366
4422
  if (!d) return "";
@@ -4924,6 +4980,7 @@ function rowToRun(row) {
4924
4980
  area: row.area ?? null,
4925
4981
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
4926
4982
  model: row.model ?? null,
4983
+ review_model: row.review_model ?? null,
4927
4984
  // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
4928
4985
  // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
4929
4986
  // непонятное здесь означает «уровень не задан», а не сломанный тип.
@@ -5001,6 +5058,7 @@ CREATE TABLE IF NOT EXISTS runs (
5001
5058
  area TEXT,
5002
5059
  depends_on TEXT,
5003
5060
  model TEXT,
5061
+ review_model TEXT,
5004
5062
  effort TEXT,
5005
5063
  cost_usd REAL NOT NULL DEFAULT 0,
5006
5064
  tokens_in INTEGER NOT NULL DEFAULT 0,
@@ -5114,7 +5172,11 @@ CREATE TABLE IF NOT EXISTS imports (
5114
5172
  "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
5115
5173
  // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
5116
5174
  // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
5117
- "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"
5118
5180
  ]) {
5119
5181
  try {
5120
5182
  this.db.exec(alter);
@@ -5134,7 +5196,7 @@ CREATE TABLE IF NOT EXISTS imports (
5134
5196
  createRun(input) {
5135
5197
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
5136
5198
  const res = this.db.prepare(
5137
- "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, effort, decision, resolves_run_id, reviews_run_id, verifies_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
5138
5200
  ).run(
5139
5201
  input.task_slug,
5140
5202
  name,
@@ -5147,6 +5209,7 @@ CREATE TABLE IF NOT EXISTS imports (
5147
5209
  input.area ?? null,
5148
5210
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
5149
5211
  input.model ?? null,
5212
+ input.review_model ?? null,
5150
5213
  input.effort ?? null,
5151
5214
  // усилие рассуждения (TASWM-395); null → разрешит спавн
5152
5215
  input.decision ? JSON.stringify(input.decision) : null,
@@ -6306,6 +6369,7 @@ var SwarmClient = class _SwarmClient {
6306
6369
  if (patch.status === "running") out.executor = this.executorId;
6307
6370
  if ("area" in patch) out.area = patch.area ?? null;
6308
6371
  if ("model" in patch) out.model = patch.model ?? null;
6372
+ if ("review_model" in patch) out.reviewModel = patch.review_model ?? null;
6309
6373
  if ("effort" in patch) out.effort = patch.effort ?? null;
6310
6374
  if ("name" in patch) out.droneName = patch.name ?? null;
6311
6375
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
@@ -6379,6 +6443,10 @@ var SwarmClient = class _SwarmClient {
6379
6443
  area: input.area ?? null,
6380
6444
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
6381
6445
  model: input.model ?? null,
6446
+ // TASWM-451: чем проверять эту работу. У прогона исполнителя приезжает из задачи
6447
+ // (claim в проде), у локально заведённого ревьюера/верификатора — наследством от
6448
+ // проверяемого прогона, которое ставит spawnReviewer/spawnVerifier.
6449
+ review_model: input.review_model ?? null,
6382
6450
  // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
6383
6451
  // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
6384
6452
  effort: input.effort ?? null,
@@ -6437,6 +6505,8 @@ var SwarmClient = class _SwarmClient {
6437
6505
  status: "queued",
6438
6506
  area: d.area,
6439
6507
  model: d.model,
6508
+ review_model: d.review_model,
6509
+ // TASWM-451: чем проверять — едет рядом с моделью
6440
6510
  effort: d.effort,
6441
6511
  // усилие рассуждения (TASWM-395), если пинили на спавне
6442
6512
  name: d.name,
@@ -7564,7 +7634,7 @@ var SwarmClient = class _SwarmClient {
7564
7634
  *
7565
7635
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
7566
7636
  */
7567
- async heartbeat(daemonVersion, pause, capacity, resources, storm) {
7637
+ async heartbeat(daemonVersion, pause, capacity, resources, storm, signIn) {
7568
7638
  try {
7569
7639
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
7570
7640
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -7574,6 +7644,10 @@ var SwarmClient = class _SwarmClient {
7574
7644
  args.paused = !!pause?.paused;
7575
7645
  if (pause?.reason) args.pauseReason = pause.reason;
7576
7646
  }
7647
+ if (signIn) {
7648
+ args.signedIn = signIn.signedIn;
7649
+ if (signIn.webUrl) args.webUrl = signIn.webUrl;
7650
+ }
7577
7651
  if (storm !== void 0 && storm !== null) {
7578
7652
  args.storm = storm.active;
7579
7653
  if (storm.reason) args.stormReason = storm.reason;
@@ -7805,6 +7879,8 @@ var SwarmClient = class _SwarmClient {
7805
7879
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
7806
7880
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
7807
7881
  review_cycle: 0,
7882
+ // TASWM-451: модель ПРОВЕРКИ — прод-поле, заполняется ниже из ответа (как base_branch).
7883
+ review_model: null,
7808
7884
  task_title: null,
7809
7885
  task_desc: null,
7810
7886
  task_url: null,
@@ -7857,6 +7933,7 @@ var SwarmClient = class _SwarmClient {
7857
7933
  local.area = pr.area ?? null;
7858
7934
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
7859
7935
  local.model = pr.model ?? null;
7936
+ local.review_model = pr.reviewModel ?? null;
7860
7937
  local.effort = toEffortLevel(pr.effort);
7861
7938
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
7862
7939
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
@@ -8895,12 +8972,26 @@ function pick(v) {
8895
8972
  const s = typeof v === "string" ? v.trim() : "";
8896
8973
  return s.length > 0 ? s : null;
8897
8974
  }
8975
+ function isReviewKind(kind) {
8976
+ return kind === "reviewer" || kind === "verifier";
8977
+ }
8978
+ function modelRoleOf(kind) {
8979
+ return isReviewKind(kind) ? "review" : "executor";
8980
+ }
8898
8981
  function forKind(kind, swarm, norm) {
8899
8982
  if (!swarm) return null;
8900
- 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) };
8901
8992
  }
8902
8993
  function resolveRunModel(run, swarm, env = process.env) {
8903
- return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8994
+ return resolveRunModelRef(run, swarm, env).model;
8904
8995
  }
8905
8996
  function resolveRunEffort(run, swarm, env = process.env) {
8906
8997
  return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
@@ -10122,11 +10213,16 @@ import { z } from "zod";
10122
10213
  init_src();
10123
10214
 
10124
10215
  // src/reviewCycle.ts
10216
+ init_src();
10125
10217
  function reviewTransitionPatch(run, prUrl) {
10126
- const isFixIteration = run.review_verdict === "changes";
10218
+ const isFixIteration = run.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run.verify_verdict));
10127
10219
  if (isFixIteration) {
10128
10220
  const cycle = (run.review_cycle ?? 0) + 1;
10129
- 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
+ };
10130
10226
  }
10131
10227
  return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
10132
10228
  }
@@ -12201,7 +12297,7 @@ async function changedPaths(worktree, opts = {}) {
12201
12297
  // package.json
12202
12298
  var package_default = {
12203
12299
  name: "@taskless-app/swarm",
12204
- version: "0.6.13",
12300
+ version: "0.6.14",
12205
12301
  type: "module",
12206
12302
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12207
12303
  keywords: [
@@ -12251,7 +12347,7 @@ var package_default = {
12251
12347
  execa: "^10.0.0",
12252
12348
  zod: "3.25.76"
12253
12349
  },
12254
- gitHead: "ea3913bf66c52d730c25ad62b7090621a5006ee7"
12350
+ gitHead: "a3b11498dbed5db3642fe4fc83bcd2054efde055"
12255
12351
  };
12256
12352
 
12257
12353
  // src/version.ts
@@ -15212,6 +15308,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
15212
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.",
15213
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.`,
15214
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.",
15215
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.",
15216
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.",
15217
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."
@@ -16804,7 +16901,7 @@ async function tickOnce(deps) {
16804
16901
  } else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
16805
16902
  await handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap);
16806
16903
  }
16807
- 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)) {
16808
16905
  continue;
16809
16906
  }
16810
16907
  const reviewerGatesOk = !st.conflicting && !st.isDraft && !!deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
@@ -17227,8 +17324,14 @@ async function replayDecision(deps, qid, chosen, by = "human") {
17227
17324
  repo: old.repo,
17228
17325
  area: old.area,
17229
17326
  decision: { question: q.text, options: q.options, chosen, prior: q.answer, priorState: old.status },
17230
- workspace_id: old.workspace_id
17327
+ workspace_id: old.workspace_id,
17231
17328
  // переигровка той же задачи — тот же воркспейс (TASWM-288)
17329
+ // TASWM-451: та же ЗАДАЧА — те же модели. Переигровка это продолжение той же работы, а не
17330
+ // новая: без переноса любой вопрос дрона оператору молча возвращал бы задачу на дефолт
17331
+ // роли, и выбор постановщика («эту делаем сильной моделью») терялся бы ровно там, где
17332
+ // работа оказалась сложной. Тот же довод, что у area и workspace_id строкой выше.
17333
+ model: old.model,
17334
+ review_model: old.review_model
17232
17335
  });
17233
17336
  deps.db.updateRun(old.id, { status: "superseded" });
17234
17337
  deps.db.supersede(old.id, fresh.id, by);
@@ -17256,8 +17359,13 @@ async function resolveConflict(deps, reviewRun) {
17256
17359
  area: reviewRun.area,
17257
17360
  // держим area-lock, пока чиним — не начинаем конфликтующее в зоне
17258
17361
  resolves_run_id: reviewRun.id,
17259
- workspace_id: reviewRun.workspace_id
17362
+ workspace_id: reviewRun.workspace_id,
17260
17363
  // та же задача, тот же воркспейс (TASWM-288)
17364
+ // TASWM-451: резолвер доделывает ТУ ЖЕ работу — разводит конфликт в её же ветке, и
17365
+ // модель ему нужна та же, что выбрали задаче. Модель проверки несём дальше, чтобы её не
17366
+ // потеряли проверяющие, которых поднимут уже по прогону резолвера.
17367
+ model: reviewRun.model,
17368
+ review_model: reviewRun.review_model
17261
17369
  });
17262
17370
  const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17263
17371
  try {
@@ -17304,6 +17412,11 @@ async function spawnReviewer(deps, reviewRun, files) {
17304
17412
  task_slug: reviewRun.task_slug,
17305
17413
  repo,
17306
17414
  kind: "reviewer",
17415
+ // TASWM-451: ЧЕМ проверять — решение постановщика, приехавшее с задачей на прогон
17416
+ // исполнителя. Пин, а не «спросим настройки на спавне»: ревьюер заводится локально, и
17417
+ // задача ему недоступна. Пусто в задаче → null → дефолт роли (модель проверки).
17418
+ model: reviewRun.review_model,
17419
+ review_model: reviewRun.review_model,
17307
17420
  reviews_run_id: reviewRun.id,
17308
17421
  review_cycle: reviewRun.review_cycle,
17309
17422
  // штамп цикла (авто-ревью v2, TASWM-162): reviewerCountFor текущего цикла
@@ -17376,6 +17489,11 @@ async function spawnVerifier(deps, verifiedRun, files) {
17376
17489
  task_slug: verifiedRun.task_slug,
17377
17490
  repo,
17378
17491
  kind: "verifier",
17492
+ // TASWM-451: та же модель проверки, что у ревьюера, и из того же источника — задачи.
17493
+ // Две проверки одного PR должны стоить одинаково, иначе одна из них незаметно уедет
17494
+ // на модель исполнителя (ровно это и стоило $45.94 на TAS-524).
17495
+ model: verifiedRun.review_model,
17496
+ review_model: verifiedRun.review_model,
17379
17497
  verifies_run_id: verifiedRun.id,
17380
17498
  review_cycle: verifiedRun.review_cycle,
17381
17499
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
@@ -17746,7 +17864,17 @@ async function runDaemon(opts) {
17746
17864
  control.state ?? null,
17747
17865
  lastOwnCapacity,
17748
17866
  hostResourcesForProd(),
17749
- { active: stormActive, reason: stormStartedAt ? `\u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 ${stormStartedAt} UTC` : null }
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}` }
17750
17878
  );
17751
17879
  if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
17752
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.13",
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": "ea3913bf66c52d730c25ad62b7090621a5006ee7",
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",