@quiel/cli 0.5.2 → 0.6.0

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/README.md +7 -0
  2. package/dist/cli.js +2002 -1331
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -372,35 +372,7 @@ var attachmentSchema = z7.object({
372
372
 
373
373
  // ../shared/src/dto/auth.ts
374
374
  import { z as z8 } from "zod";
375
- var PASSWORD_MIN_LENGTH = 10;
376
- var PASSWORD_MAX_LENGTH = 128;
377
- var PasswordSchema = z8.string().min(PASSWORD_MIN_LENGTH, `\u041F\u0430\u0440\u043E\u043B\u044C \u043A\u043E\u0440\u043E\u0447\u0435 ${PASSWORD_MIN_LENGTH} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432`).max(PASSWORD_MAX_LENGTH, `\u041F\u0430\u0440\u043E\u043B\u044C \u0434\u043B\u0438\u043D\u043D\u0435\u0435 ${PASSWORD_MAX_LENGTH} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432`);
378
375
  var EmailSchema = z8.string().trim().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u043E\u0447\u0442\u0443").email("\u041F\u043E\u0445\u043E\u0436\u0435, \u0432 \u0430\u0434\u0440\u0435\u0441\u0435 \u043E\u043F\u0435\u0447\u0430\u0442\u043A\u0430").toLowerCase();
379
- var RegisterSchema = z8.object({
380
- name: z8.string().trim().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F").max(120),
381
- email: EmailSchema,
382
- password: PasswordSchema
383
- }).refine(
384
- (value) => {
385
- const localPart = value.email.split("@")[0] ?? "";
386
- return localPart.length < 3 || !value.password.toLowerCase().includes(localPart);
387
- },
388
- { message: "\u041F\u0430\u0440\u043E\u043B\u044C \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0442\u044C \u0430\u0434\u0440\u0435\u0441 \u043F\u043E\u0447\u0442\u044B", path: ["password"] }
389
- );
390
- var LoginSchema = z8.object({
391
- email: EmailSchema,
392
- // На входе НЕ применяем правила длины: пароль мог быть задан раньше, по
393
- // другим правилам, и «слишком короткий» на форме входа — это отказ в
394
- // доступе законному владельцу, а заодно подсказка перебирающему, что
395
- // проверять длину не нужно.
396
- password: z8.string().min(1, "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C")
397
- });
398
- var ForgotPasswordSchema = z8.object({ email: EmailSchema });
399
- var ResetPasswordSchema = z8.object({
400
- token: z8.string().min(1),
401
- password: PasswordSchema
402
- });
403
- var ResendVerificationSchema = z8.object({ email: EmailSchema });
404
376
 
405
377
  // ../shared/src/dto/milestone.ts
406
378
  import { z as z9 } from "zod";
@@ -499,14 +471,8 @@ var ProfileNameSchema = z11.string().trim().min(1, "\u0423\u043A\u0430\u0436\u04
499
471
  var UpdateProfileSchema = z11.object({
500
472
  name: ProfileNameSchema
501
473
  });
502
- var ChangePasswordSchema = z11.object({
503
- currentPassword: z11.string().min(1).max(200).optional(),
504
- newPassword: PasswordSchema
505
- });
506
474
  var RequestEmailChangeSchema = z11.object({
507
- newEmail: EmailSchema,
508
- /** Нужен, если у аккаунта есть пароль — см. `ChangePasswordSchema`. */
509
- currentPassword: z11.string().min(1).max(200).optional()
475
+ newEmail: EmailSchema
510
476
  });
511
477
  var ConfirmEmailChangeSchema = z11.object({
512
478
  token: z11.string().min(1)
@@ -815,6 +781,26 @@ var ProjectSettingsSchema = z14.object({
815
781
  * Пусто — задача заводится без роли и видна всей команде проекта.
816
782
  */
817
783
  reviewRoleKey: z14.string().trim().min(1).max(60).optional(),
784
+ /**
785
+ * Канал Slack, куда уходят события проекта (SL-04, ADR-007).
786
+ *
787
+ * КАНАЛ НА ПРОЕКТ, А НЕ НА ОРГАНИЗАЦИЮ. Установка одна на компанию —
788
+ * так устроен Slack, — но лента у каждого проекта своя: в организации
789
+ * с пятью проектами общий канал превращается в шум, который выключают
790
+ * через неделю.
791
+ *
792
+ * ПУСТО — ПОДДЕРЖИВАЕМЫЙ РЕЖИМ, А НЕ ПОЛОМКА. Проект без канала
793
+ * работает на личных сообщениях: подтверждения и вопросы всё равно
794
+ * приходят названному человеку в личку. Канал добавляет ОБЩУЮ
795
+ * видимость, а не доставку.
796
+ */
797
+ slackChannelId: z14.string().trim().min(1).max(40).optional(),
798
+ /**
799
+ * Имя канала на момент выбора — только чтобы показать человеку
800
+ * «#релизы», а не `C09ABCDEF`. Slack допускает переименование, поэтому
801
+ * истина — идентификатор, а это подпись к нему.
802
+ */
803
+ slackChannelName: z14.string().trim().min(1).max(120).optional(),
818
804
  maxParallelTasksPerAgent: z14.number().int().positive().default(1),
819
805
  leaseTtlSec: z14.number().int().positive().default(120),
820
806
  taskTimeoutMin: z14.number().int().positive().default(90),
@@ -1317,13 +1303,22 @@ var taskPayloadLeaseSchema = z23.object({
1317
1303
  resumeToken: z23.string().min(1),
1318
1304
  expiresAt: z23.string().datetime()
1319
1305
  });
1306
+ var taskPayloadContinueFromSchema = z23.object({
1307
+ branchName: z23.string().min(1),
1308
+ sha: z23.string().optional(),
1309
+ /** Короткая записка прошлого исполнителя «что успел». */
1310
+ note: z23.string().optional(),
1311
+ savedAt: z23.string()
1312
+ });
1320
1313
  var taskPayloadSchema = z23.object({
1321
1314
  task: taskPayloadTaskSchema,
1322
1315
  project: taskPayloadProjectSchema,
1323
1316
  dependencies: z23.array(taskPayloadDependencySchema),
1324
1317
  openQuestions: z23.array(taskPayloadOpenQuestionSchema),
1325
1318
  trust: taskPayloadTrustSchema,
1326
- lease: taskPayloadLeaseSchema
1319
+ lease: taskPayloadLeaseSchema,
1320
+ /** Есть только у задач, которые уже начинали и прервали. */
1321
+ continueFrom: taskPayloadContinueFromSchema.optional()
1327
1322
  });
1328
1323
  var taskClaimPayloadSchema = z23.object({
1329
1324
  /**
@@ -1440,7 +1435,21 @@ var contextPayloadSchema = z23.object({
1440
1435
  });
1441
1436
  var agentRateLimitedPayloadSchema = z23.object({
1442
1437
  retryAfterSec: z23.number().int().positive().optional(),
1443
- taskId: z23.string().min(1).optional()
1438
+ taskId: z23.string().min(1).optional(),
1439
+ /**
1440
+ * Когда лимит сбросится (LIM-06) — разобрано клиентом из текста самой
1441
+ * ошибки Claude Code («resets 3:45pm»).
1442
+ *
1443
+ * НЕ ПУТАТЬ С `retryAfterSec`. Тот — сколько секунд подождать, как
1444
+ * посчитал сам Claude Code, и его часто нет вовсе. Этот — абсолютный
1445
+ * момент, уже приведённый к UTC клиентом: в сообщении время местное и
1446
+ * без пояса, и разобрать его может только тот, кто стоит на той же
1447
+ * машине.
1448
+ *
1449
+ * Необязательное: формат сообщения может смениться, и тогда клиент
1450
+ * честно ничего не присылает, а не гадает.
1451
+ */
1452
+ resetsAt: z23.string().datetime().optional()
1444
1453
  });
1445
1454
  var agentRateLimitedAckSchema = z23.object({
1446
1455
  ok: z23.boolean(),
@@ -1520,6 +1529,18 @@ var taskUsagePayloadSchema = z23.object({
1520
1529
  entries: z23.array(taskUsageEntrySchema).min(1).max(10)
1521
1530
  });
1522
1531
  var taskUsageAckSchema = z23.object({ ok: z23.boolean() });
1532
+ var taskWorkSavedPayloadSchema = z23.object({
1533
+ taskId: z23.string().min(1),
1534
+ /** Ветка, в которую ушла незаконченная работа. */
1535
+ branchName: z23.string().min(1).max(255),
1536
+ sha: z23.string().min(7).max(64).optional(),
1537
+ /**
1538
+ * Короткая записка «что успел» — её увидит следующий исполнитель.
1539
+ * Потолок жёсткий: это подпись к ветке, а не отчёт о работе.
1540
+ */
1541
+ note: z23.string().max(500).optional()
1542
+ });
1543
+ var taskWorkSavedAckSchema = z23.object({ ok: z23.boolean() });
1523
1544
  var sessionLimitReachedPayloadSchema = z23.object({
1524
1545
  tasksCompletedInSession: z23.number().int().nonnegative()
1525
1546
  });
@@ -1533,7 +1554,17 @@ var taskCreatePayloadSchema = z23.object({
1533
1554
  executor: z23.enum(executorValues2),
1534
1555
  blocksCurrent: z23.boolean(),
1535
1556
  acceptance: z23.string().optional(),
1536
- labels: z23.array(z23.string().min(1)).optional()
1557
+ labels: z23.array(z23.string().min(1)).optional(),
1558
+ /**
1559
+ * Задачи, от которых новая зависит, — связи ставятся сразу.
1560
+ *
1561
+ * Без этого поля менеджеру, разложившему спецификацию на двадцать
1562
+ * пять задач, приходилось идти по списку вторым проходом. Связь,
1563
+ * замыкающая кольцо, молча пропускается: задача уже создана, и ронять
1564
+ * создание из-за одной плохой связи значило бы терять работу целиком.
1565
+ * Ответ честно говорит, сколько связей поставлено.
1566
+ */
1567
+ dependsOn: z23.array(z23.string().min(1)).max(50).optional()
1537
1568
  }).refine((v) => Boolean(v.roleKey) !== Boolean(v.assigneeId), {
1538
1569
  message: "\u041D\u0443\u0436\u0435\u043D \u0440\u043E\u0432\u043D\u043E \u043E\u0434\u0438\u043D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: roleKey \u0418\u041B\u0418 assigneeId"
1539
1570
  });
@@ -1552,7 +1583,16 @@ var taskBriefSchema = z23.object({
1552
1583
  var taskCreateAckSchema = z23.object({
1553
1584
  task: taskBriefSchema,
1554
1585
  /** Во что ушла ТЕКУЩАЯ задача автора, если `blocksCurrent: true`. */
1555
- currentTaskStatus: z23.enum(taskStatusValues3).optional()
1586
+ currentTaskStatus: z23.enum(taskStatusValues3).optional(),
1587
+ /**
1588
+ * Сколько связей из `dependsOn` поставлено.
1589
+ *
1590
+ * Может быть меньше запрошенного: связь, замыкающая кольцо, или
1591
+ * задача из чужого проекта пропускаются молча — задача уже создана, и
1592
+ * ронять создание из-за плохой связи значило бы терять работу.
1593
+ * Поэтому число, а не «ок»: по нему видно, что не всё прошло.
1594
+ */
1595
+ dependenciesLinked: z23.number().int().nonnegative().optional()
1556
1596
  });
1557
1597
  var taskListPayloadSchema = z23.object({
1558
1598
  status: z23.array(z23.enum(taskStatusValues3)).optional(),
@@ -1564,6 +1604,57 @@ var taskListPayloadSchema = z23.object({
1564
1604
  var taskListAckSchema = z23.object({
1565
1605
  tasks: z23.array(taskBriefSchema)
1566
1606
  });
1607
+ var taskPlanPayloadSchema = z23.object({
1608
+ taskId: z23.string().min(1),
1609
+ patch: z23.object({
1610
+ title: z23.string().min(1).max(300).optional(),
1611
+ descriptionMd: z23.string().max(1e5).optional(),
1612
+ acceptance: z23.string().max(2e4).nullable().optional(),
1613
+ type: z23.enum(taskTypeValues2).optional(),
1614
+ priority: z23.enum(priorityValues2).optional(),
1615
+ roleKey: z23.string().min(1).max(60).nullable().optional(),
1616
+ assigneeId: z23.string().min(1).nullable().optional(),
1617
+ milestoneId: z23.string().min(1).nullable().optional(),
1618
+ labels: z23.array(z23.string().min(1)).max(30).optional(),
1619
+ estimateMin: z23.number().int().positive().nullable().optional()
1620
+ }).refine((patch) => Object.keys(patch).length > 0, {
1621
+ message: "\u041D\u0443\u0436\u043D\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u043D\u043E \u043F\u043E\u043B\u0435"
1622
+ })
1623
+ });
1624
+ var taskPlanAckSchema = z23.object({
1625
+ task: taskBriefSchema
1626
+ });
1627
+ var taskLinkPayloadSchema = z23.object({
1628
+ /** Задача, которая блокирует (её делают первой). */
1629
+ blockerId: z23.string().min(1),
1630
+ /** Задача, которая ждёт (§7.3: `fromId` — «эта задача зависит от…»). */
1631
+ blockedId: z23.string().min(1),
1632
+ /** `BLOCKS` держит очередь, `RELATES` — просто связь «см. также». */
1633
+ kind: z23.enum(["BLOCKS", "RELATES"]).default("BLOCKS")
1634
+ });
1635
+ var taskLinkAckSchema = z23.object({
1636
+ linked: z23.boolean(),
1637
+ /** Задача-ждущая ушла в BLOCKED прямо сейчас (была IN_PROGRESS). */
1638
+ blockedNow: z23.boolean()
1639
+ });
1640
+ var taskUnlinkPayloadSchema = z23.object({
1641
+ blockerId: z23.string().min(1),
1642
+ blockedId: z23.string().min(1)
1643
+ });
1644
+ var taskUnlinkAckSchema = z23.object({
1645
+ /** `false` — такой связи и не было; это не ошибка, а отчёт. */
1646
+ unlinked: z23.boolean()
1647
+ });
1648
+ var taskPeekPayloadSchema = z23.object({});
1649
+ var taskPeekAckSchema = z23.object({
1650
+ /**
1651
+ * Нашлась задача, которую диспетчер выдал бы прямо сейчас.
1652
+ *
1653
+ * Подсказка, а не обещание: пока агент просыпается, задачу может взять
1654
+ * другой. Цена ошибки — один пустой `next_task`.
1655
+ */
1656
+ available: z23.boolean()
1657
+ });
1567
1658
  var taskUpdatePayloadSchema = z23.object({
1568
1659
  taskId: z23.string().min(1),
1569
1660
  patch: z23.object({
@@ -1845,12 +1936,28 @@ var ImportTasksRequestSchema = z26.object({
1845
1936
  var IntegrationStatus = {
1846
1937
  /** Работает и проверена вживую. */
1847
1938
  READY: "ready",
1939
+ /**
1940
+ * Сделана целиком, часть пути пройдена вживую, но не весь.
1941
+ *
1942
+ * Заведено для Slack 23 сентября 2026. «Скоро» про него стало враньём в
1943
+ * тот день, когда владелец установил приложение, привязал себя командой
1944
+ * `/quiel link` и выбрал каналы двум проектам — всё это прошло на
1945
+ * настоящем рабочем пространстве. «Работает и проверена вживую» тоже
1946
+ * неправда: ни одно сообщение в канал ещё не наблюдалось, потому что в
1947
+ * проектах не было событий.
1948
+ *
1949
+ * Тот же приём, что `support: 'documented'` у видов агентских программ
1950
+ * (`agents/kinds.ts`): пометка снимается после живого прогона, а не
1951
+ * после того, как код написан.
1952
+ */
1953
+ BETA: "beta",
1848
1954
  /** Запланирована, но ещё не сделана. Показываем, чтобы не обещать голосом. */
1849
1955
  PLANNED: "planned"
1850
1956
  };
1851
1957
  var IntegrationKey = {
1852
1958
  GITHUB: "github",
1853
1959
  TELEGRAM: "telegram",
1960
+ SLACK: "slack",
1854
1961
  WEBHOOKS: "webhooks"
1855
1962
  };
1856
1963
  var INTEGRATIONS = [
@@ -1868,6 +1975,22 @@ var INTEGRATIONS = [
1868
1975
  scope: "both",
1869
1976
  requiresOrgAdmin: false
1870
1977
  },
1978
+ {
1979
+ // ПЕРВАЯ ОБЩАЯ поверхность продукта: канал проекта, где команда
1980
+ // видит работу агентов, не открывая дашборд (ADR-007). Остальные
1981
+ // каналы уведомлений личные.
1982
+ //
1983
+ // Статус `beta`, а не `planned` и не `ready`. Установка, привязка
1984
+ // человека командой `/quiel link` и выбор канала проекту прошли на
1985
+ // настоящем рабочем пространстве 23 сентября 2026 — «скоро» про это
1986
+ // было бы враньём. Но ни одно сообщение в канал ещё не наблюдалось:
1987
+ // в проектах не было событий. Пометка снимается после первого
1988
+ // дошедшего сообщения, а не после того, как код написан.
1989
+ key: IntegrationKey.SLACK,
1990
+ status: IntegrationStatus.BETA,
1991
+ scope: "org",
1992
+ requiresOrgAdmin: true
1993
+ },
1871
1994
  {
1872
1995
  // Одна механика вместо десяти интеграций: Zapier, n8n, Make и любой
1873
1996
  // свой эндпоинт. Пока не сделана — и список об этом честно говорит.
@@ -1884,6 +2007,15 @@ var BY_KEY = new Map(INTEGRATIONS.map((info) => [info.key, info]));
1884
2007
  var NotificationChannel = {
1885
2008
  INAPP: "inapp",
1886
2009
  TELEGRAM: "telegram",
2010
+ /**
2011
+ * Личное сообщение в Slack (SL-06).
2012
+ *
2013
+ * ИМЕННО ЛИЧНОЕ, а не канал проекта. Канал — общая видимость и живёт
2014
+ * отдельно (`slack-channel` в воркере): он не адресован никому и не
2015
+ * зависит от настроек человека. Смешай мы их в одном канале доставки —
2016
+ * выключив «Slack», человек выключил бы и ленту всей команды.
2017
+ */
2018
+ SLACK: "slack",
1887
2019
  EMAIL: "email",
1888
2020
  PUSH: "push"
1889
2021
  };
@@ -1897,18 +2029,21 @@ var NotificationUrgency = {
1897
2029
  var ALL = [
1898
2030
  NotificationChannel.INAPP,
1899
2031
  NotificationChannel.TELEGRAM,
2032
+ NotificationChannel.SLACK,
1900
2033
  NotificationChannel.EMAIL,
1901
2034
  NotificationChannel.PUSH
1902
2035
  ];
1903
2036
  var FAST = [
1904
2037
  NotificationChannel.INAPP,
1905
2038
  NotificationChannel.TELEGRAM,
2039
+ NotificationChannel.SLACK,
1906
2040
  NotificationChannel.PUSH
1907
2041
  ];
1908
2042
  var QUIET = [NotificationChannel.INAPP];
1909
2043
  var SLOW = [
1910
2044
  NotificationChannel.INAPP,
1911
2045
  NotificationChannel.TELEGRAM,
2046
+ NotificationChannel.SLACK,
1912
2047
  NotificationChannel.EMAIL
1913
2048
  ];
1914
2049
  function defaultsFor(urgency) {
@@ -1972,8 +2107,20 @@ var NOTIFICATION_KINDS = [
1972
2107
  kind(
1973
2108
  "agent_session_limit",
1974
2109
  "\u0421\u0435\u0441\u0441\u0438\u044F \u0430\u0433\u0435\u043D\u0442\u0430 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u0430",
1975
- "\u0410\u0433\u0435\u043D\u0442 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u043B \u043F\u043E\u0434\u0440\u044F\u0434 \u0441\u0442\u043E\u043B\u044C\u043A\u043E \u0437\u0430\u0434\u0430\u0447, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E \u0434\u043E \u043E\u0447\u0438\u0441\u0442\u043A\u0438 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430.",
1976
- NotificationUrgency.INFORMATIONAL
2110
+ "\u0410\u0433\u0435\u043D\u0442 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u043B \u043F\u043E\u0434\u0440\u044F\u0434 \u0441\u0442\u043E\u043B\u044C\u043A\u043E \u0437\u0430\u0434\u0430\u0447, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E \u0434\u043E \u043E\u0447\u0438\u0441\u0442\u043A\u0438 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430, \u0438 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B\u0441\u044F.",
2111
+ // ACTIONABLE, А НЕ INFORMATIONAL, И РАЗНИЦА НЕ В СЛОВЕ.
2112
+ //
2113
+ // У informational push, почта и Telegram выключены по умолчанию —
2114
+ // уведомление оставалось в колокольчике. 24 сентября 2026 владелец
2115
+ // обнаружил, что агент простоял ночь: он остановился по лимиту и
2116
+ // ждал `/clear`, а узнать об этом было неоткуда. Проверено в базе:
2117
+ // у трёх таких записей стояло `{push: false, email: false,
2118
+ // telegram: false}`.
2119
+ //
2120
+ // Уведомление, которое ТРЕБУЕТ ДЕЙСТВИЯ человека, и есть
2121
+ // определение actionable: без `/clear` очередь стоит, даже если
2122
+ // режим проекта автономный.
2123
+ NotificationUrgency.ACTIONABLE
1977
2124
  ),
1978
2125
  kind(
1979
2126
  "task_auto_accepted",
@@ -1995,6 +2142,7 @@ import { z as z27 } from "zod";
1995
2142
  var NotificationChannelFlagsSchema = z27.object({
1996
2143
  inapp: z27.boolean().optional(),
1997
2144
  telegram: z27.boolean().optional(),
2145
+ slack: z27.boolean().optional(),
1998
2146
  email: z27.boolean().optional(),
1999
2147
  push: z27.boolean().optional()
2000
2148
  }).partial();
@@ -3271,7 +3419,8 @@ import { z as z29 } from "zod";
3271
3419
  var HookCommandEntrySchema = z29.object({
3272
3420
  type: z29.literal("command"),
3273
3421
  command: z29.string().min(1),
3274
- timeout: z29.number().int().positive().optional()
3422
+ timeout: z29.number().int().positive().optional(),
3423
+ asyncRewake: z29.boolean().optional()
3275
3424
  }).passthrough();
3276
3425
  var HookMatcherGroupSchema = z29.object({
3277
3426
  matcher: z29.string().optional(),
@@ -3291,6 +3440,14 @@ var CLI_BIN = "quiel";
3291
3440
  var HOOK_SPECS = [
3292
3441
  { event: "SessionStart", command: `${CLI_BIN} hook session-start` },
3293
3442
  { event: "Stop", command: `${CLI_BIN} hook stop`, timeout: 10 },
3443
+ // ВТОРАЯ КОМАНДА НА ТОМ ЖЕ СОБЫТИИ, И ОНА НЕ ДУБЛЬ. `hook stop` решает,
3444
+ // можно ли остановиться; эта — что будет, ПОКА агент стоит. Без неё
3445
+ // разрешённый простой означал бы молчание до прихода человека.
3446
+ //
3447
+ // Выходит мгновенно с кодом 0, если режим ручной, задача на руках или
3448
+ // другой ожидатель уже работает, — поэтому висит она только тогда,
3449
+ // когда агенту действительно нечего делать.
3450
+ { event: "Stop", command: `${CLI_BIN} wait-for-task`, asyncRewake: true },
3294
3451
  {
3295
3452
  event: "StopFailure",
3296
3453
  matcher: "rate_limit|overloaded",
@@ -3313,7 +3470,8 @@ function upsertHookSpec(groups, spec) {
3313
3470
  const commandEntry = {
3314
3471
  type: "command",
3315
3472
  command: spec.command,
3316
- ...spec.timeout !== void 0 ? { timeout: spec.timeout } : {}
3473
+ ...spec.timeout !== void 0 ? { timeout: spec.timeout } : {},
3474
+ ...spec.asyncRewake !== void 0 ? { asyncRewake: spec.asyncRewake } : {}
3317
3475
  };
3318
3476
  const groupIndex = groups.findIndex((g) => sameMatcher(g.matcher, spec.matcher));
3319
3477
  if (groupIndex === -1) {
@@ -3423,7 +3581,22 @@ var CurrentTaskSchema = z30.object({
3423
3581
  * Необязательно: состояния, записанные прежними версиями клиента, поля
3424
3582
  * не имеют — тогда `submit_work` работает как раньше, в режиме DIRECT.
3425
3583
  */
3426
- gitMode: z30.enum([GitMode.DIRECT, GitMode.PR]).optional()
3584
+ gitMode: z30.enum([GitMode.DIRECT, GitMode.PR]).optional(),
3585
+ /**
3586
+ * Имя ветки задачи и префикс сообщения коммита — как их посчитал
3587
+ * СЕРВЕР (`project.branchName`, `project.commitPrefix`, §10.3 ТЗ).
3588
+ *
3589
+ * Добавлены для спасения незаконченной работы (LIM-01): когда агент
3590
+ * упирается в лимит подписки, коммитить и отправлять надо уже без
3591
+ * него — он в этот момент ничего не может. Значит и ветку, и префикс
3592
+ * надо помнить заранее, а не спрашивать в момент прерывания.
3593
+ *
3594
+ * Необязательны: у состояний, записанных прежними версиями клиента,
3595
+ * полей нет — тогда работа спасается только коммитом (без push) или
3596
+ * не спасается вовсе, и об этом честно сообщается.
3597
+ */
3598
+ branchName: z30.string().min(1).optional(),
3599
+ commitPrefix: z30.string().min(1).optional()
3427
3600
  });
3428
3601
  var TrustCacheSchema = z30.object({
3429
3602
  profileKey: z30.string().min(1),
@@ -3465,6 +3638,30 @@ var ClientStateSchema = z30.object({
3465
3638
  tasksCompletedInSession: z30.number().int().nonnegative().default(0),
3466
3639
  /** Кэш `ProjectSettings.contextResetAfterTasks` (§7.2 ТЗ) — сколько задач подряд можно сдать, прежде чем `Stop`-хук попросит `/clear`. */
3467
3640
  contextResetAfterTasks: z30.number().int().positive().default(DEFAULT_CONTEXT_RESET_AFTER_TASKS),
3641
+ /**
3642
+ * Идентификаторы задач, сданных в этой сессии, — а не одно число.
3643
+ *
3644
+ * Счётчик `tasksCompletedInSession` увеличивается на КАЖДУЮ успешную
3645
+ * сдачу, и задача, возвращённая с ревью и сданная повторно, считалась
3646
+ * второй. Владелец увидел «Сессия выполнила 9 задач» при лимите 8 и
3647
+ * справедливо решил, что счётчик сломан: он не сломан, он считает
3648
+ * сдачи, а сообщение называет их задачами.
3649
+ *
3650
+ * Храним ключи, чтобы говорить правду в обоих числах.
3651
+ */
3652
+ completedTaskIds: z30.array(z30.string()).default([]),
3653
+ /**
3654
+ * Когда `next_task` в последний раз ответил «задач нет».
3655
+ *
3656
+ * БЕЗ ЭТОГО ПОЛЯ `Stop`-ХУК НЕ ОТЛИЧАЕТ ПУСТУЮ ОЧЕРЕДЬ ОТ ЛЕНИ. Он
3657
+ * безусловно требовал «вызови next_task», и при пустой очереди агент
3658
+ * крутил этот цикл часами: в боевом проекте очередь пустовала по
3659
+ * 15–25 минут подряд, и каждый круг тратил ход модели впустую.
3660
+ *
3661
+ * `null` — очередь не была пуста с начала сессии либо последняя
3662
+ * попытка что-то принесла.
3663
+ */
3664
+ queueEmptyAt: z30.string().nullable().default(null),
3468
3665
  /** Кэш стоп-паттернов/правил доверия для `PreToolUse` фазы 4. */
3469
3666
  trust: TrustCacheSchema.default(null),
3470
3667
  /** Снимок задачи на момент последнего `PreCompact` — пишет src/hooks/pre-compact.ts, читает SessionStart(source="compact"), чтобы напомнить модели, чем она занималась. */
@@ -4175,7 +4372,14 @@ var createSocket = (url, options) => io(url, {
4175
4372
  // src/hooks/audit-sender.ts
4176
4373
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e3;
4177
4374
  var DEFAULT_FLUSH_GRACE_MS = 150;
4178
- var WIRE_EVENTS = /* @__PURE__ */ new Set(["audit:command", "task:usage", "session-limit-reached"]);
4375
+ var WIRE_EVENTS = /* @__PURE__ */ new Set([
4376
+ "audit:command",
4377
+ "task:usage",
4378
+ "session-limit-reached",
4379
+ "agent:rate_limited",
4380
+ // LIM-02: о спасённой ветке сервер узнаёт отсюда же.
4381
+ "task:work_saved"
4382
+ ]);
4179
4383
  function sendHookActions(actions, options = {}) {
4180
4384
  const wireActions = actions.filter((action) => WIRE_EVENTS.has(action.type));
4181
4385
  if (wireActions.length === 0) return;
@@ -4360,1297 +4564,1528 @@ function renderHookOutput(format, hook, output) {
4360
4564
  return { stdout: "{}", stderr: "", exitCode: 0 };
4361
4565
  }
4362
4566
 
4363
- // src/socket/auth-reject.ts
4364
- var AUTH_REJECT_REASONS = /* @__PURE__ */ new Set(["missing", "invalid"]);
4365
- function isAuthRejection(error) {
4366
- const message = error instanceof Error ? error.message : String(error);
4367
- return AUTH_REJECT_REASONS.has(message.trim());
4567
+ // src/git/exec.ts
4568
+ import { execFile } from "child_process";
4569
+ var DEFAULT_TIMEOUT_MS = 12e4;
4570
+ var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
4571
+ function runGit(args, options) {
4572
+ return new Promise((resolvePromise) => {
4573
+ execFile(
4574
+ "git",
4575
+ [...args],
4576
+ {
4577
+ cwd: options.cwd,
4578
+ shell: false,
4579
+ timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
4580
+ maxBuffer: MAX_BUFFER_BYTES,
4581
+ env: {
4582
+ ...process.env,
4583
+ ...options.env,
4584
+ // Без человека за терминалом: если git вдруг решит спросить
4585
+ // логин/пароль, зависший prompt выглядел бы как неожиданное
4586
+ // поведение (требование подзадачи: «любая неожиданность —
4587
+ // понятный отказ, а не попробую починить»), а не как честный
4588
+ // отказ команды с кодом возврата.
4589
+ GIT_TERMINAL_PROMPT: "0"
4590
+ }
4591
+ },
4592
+ (error, stdout, stderr) => {
4593
+ if (!error) {
4594
+ resolvePromise({ ok: true, code: 0, signal: null, stdout, stderr });
4595
+ return;
4596
+ }
4597
+ const code = typeof error.code === "number" ? error.code : null;
4598
+ resolvePromise({
4599
+ ok: false,
4600
+ code,
4601
+ signal: error.signal ?? null,
4602
+ stdout,
4603
+ // Если git ничего не написал в stderr (например, сам процесс не
4604
+ // запустился — ENOENT), берём текст ошибки Node, чтобы вызывающий
4605
+ // код не остался с пустым сообщением.
4606
+ stderr: stderr.length > 0 ? stderr : error.message
4607
+ });
4608
+ }
4609
+ );
4610
+ });
4368
4611
  }
4369
- function describeAuthRejection(agentId) {
4370
- return `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B\u0430 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 ${agentId}: \u043E\u043D \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D, \u043E\u0442\u043E\u0437\u0432\u0430\u043D \u0438\u043B\u0438 \u0438\u0441\u043F\u043E\u0440\u0447\u0435\u043D. \u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043D\u0435 \u043F\u043E\u043C\u043E\u0436\u0435\u0442 \u2014 \u0432\u044B\u043F\u0443\u0441\u0442\u0438 \u043D\u043E\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0430\u0433\u0435\u043D\u0442\u0430 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438 "quiel init" \u0437\u0430\u043D\u043E\u0432\u043E.`;
4612
+ function describeGitFailure(commandLabel, result) {
4613
+ const stderr = result.stderr.trim();
4614
+ const codeText = result.code === null ? "" : ` (\u043A\u043E\u0434 ${result.code})`;
4615
+ return stderr.length > 0 ? `${commandLabel}${codeText}: ${stderr}` : `${commandLabel} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439${codeText}`;
4371
4616
  }
4372
4617
 
4373
- // src/socket/backoff.ts
4374
- function nextReconnectDelayMs(attempt, options = {}) {
4375
- const baseMs = options.baseMs ?? 1e3;
4376
- const maxMs = options.maxMs ?? 3e4;
4377
- const safeAttempt = Math.max(0, Math.trunc(attempt));
4378
- const exponent = Math.min(safeAttempt, 30);
4379
- const raw = baseMs * 2 ** exponent;
4380
- return Math.min(raw, maxMs);
4618
+ // src/git/result.ts
4619
+ function gitOk(data) {
4620
+ return { ok: true, data };
4381
4621
  }
4382
-
4383
- // src/socket/client.ts
4384
- import { EventEmitter } from "events";
4385
-
4386
- // src/socket/emit-with-timeout.ts
4387
- function toError(raw, context) {
4388
- return raw instanceof Error ? raw : new Error(`${context}: ${String(raw)}`);
4622
+ function gitErr(message) {
4623
+ return { ok: false, message };
4389
4624
  }
4390
- async function emitWithAckTimeout(socket, event, payload, timeoutMs) {
4391
- return await new Promise((resolve4, reject) => {
4392
- const timer = setTimeout(() => {
4393
- reject(new Error(`${event}: \u043D\u0435\u0442 ack \u043E\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0437\u0430 ${timeoutMs} \u043C\u0441`));
4394
- }, timeoutMs);
4395
- if (typeof timer.unref === "function") timer.unref();
4396
- socket.emitWithAck(event, payload).then((value) => {
4397
- clearTimeout(timer);
4398
- resolve4(value);
4399
- }).catch((err) => {
4400
- clearTimeout(timer);
4401
- reject(toError(err, event));
4402
- });
4625
+
4626
+ // src/git/repo.ts
4627
+ async function localBranchExists(repoRoot, branch) {
4628
+ const result = await runGit(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
4629
+ cwd: repoRoot
4403
4630
  });
4631
+ return result.ok;
4404
4632
  }
4405
-
4406
- // src/socket/trust-cache.ts
4407
- var UNKNOWN_TRUST_PROFILE_KEY = "unknown";
4408
- function toTrustCache(rules, now = () => /* @__PURE__ */ new Date()) {
4409
- return {
4410
- profileKey: UNKNOWN_TRUST_PROFILE_KEY,
4411
- updatedAt: now().toISOString(),
4412
- rules
4413
- };
4633
+ async function getHeadSha(worktreeDir) {
4634
+ const result = await runGit(["rev-parse", "HEAD"], { cwd: worktreeDir });
4635
+ if (!result.ok) {
4636
+ return gitErr(describeGitFailure("git rev-parse HEAD", result));
4637
+ }
4638
+ return gitOk(result.stdout.trim());
4639
+ }
4640
+ async function currentBranch(worktreeDir) {
4641
+ const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreeDir });
4642
+ if (!result.ok) {
4643
+ return gitErr(describeGitFailure("git rev-parse --abbrev-ref HEAD", result));
4644
+ }
4645
+ return gitOk(result.stdout.trim());
4414
4646
  }
4415
4647
 
4416
- // src/socket/types.ts
4417
- function isAckError(raw) {
4418
- return typeof raw === "object" && raw !== null && "error" in raw && typeof raw.error === "object";
4648
+ // src/git/status.ts
4649
+ var CONFLICT_CODES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
4650
+ async function getWorktreeStatus(worktreeDir) {
4651
+ const result = await runGit(["status", "--porcelain=v1", "--untracked-files=all"], {
4652
+ cwd: worktreeDir
4653
+ });
4654
+ if (!result.ok) {
4655
+ return gitErr(describeGitFailure("git status", result));
4656
+ }
4657
+ const changedPaths = [];
4658
+ const conflictedPaths = [];
4659
+ for (const line of result.stdout.split("\n")) {
4660
+ if (line.length === 0) continue;
4661
+ const code = line.slice(0, 2);
4662
+ const rawPath = line.slice(3);
4663
+ const arrowIndex = rawPath.indexOf(" -> ");
4664
+ const path = arrowIndex >= 0 ? rawPath.slice(arrowIndex + 4) : rawPath;
4665
+ changedPaths.push(path);
4666
+ if (CONFLICT_CODES.has(code)) conflictedPaths.push(path);
4667
+ }
4668
+ return gitOk({ clean: changedPaths.length === 0, changedPaths, conflictedPaths });
4419
4669
  }
4420
4670
 
4421
- // src/socket/client.ts
4422
- var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
4423
- var DEFAULT_ACK_TIMEOUT_MS = 1e4;
4424
- var DEFAULT_BACKOFF_BASE_MS = 1e3;
4425
- var DEFAULT_BACKOFF_MAX_MS = 3e4;
4426
- function toError2(raw, context) {
4427
- if (raw instanceof Error) return raw;
4428
- return new Error(`${context}: ${String(raw)}`);
4671
+ // src/git/commit.ts
4672
+ function buildCommitMessage(params) {
4673
+ const subject = `${params.commitPrefix} ${params.title}`.trim();
4674
+ return `${subject}
4675
+
4676
+ ${buildTaskTrailer(params.taskKey)}
4677
+ `;
4429
4678
  }
4430
- function resolveOptions(options) {
4431
- return {
4432
- roleKeys: options.roleKeys,
4433
- cwd: options.cwd,
4434
- clientVersion: options.clientVersion,
4435
- agentVersion: options.agentVersion ?? "unknown",
4436
- agentKind: options.agentKind ?? DEFAULT_AGENT_KIND,
4437
- os: options.os ?? process.platform,
4438
- // По умолчанию — файл ИМЕННО этого агента (state/store.ts::getStateFilePath(agentId)),
4439
- // а не общий `state.json`: не трогаем src/state/**, но обязаны использовать
4440
- // его правильно (OQ-05 — состояние на агента, не на машину).
4441
- stateFilePath: options.stateFilePath ?? getStateFilePath(options.agentId),
4442
- heartbeatIntervalMs: options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
4443
- ackTimeoutMs: options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS,
4444
- backoffBaseMs: options.backoffBaseMs ?? DEFAULT_BACKOFF_BASE_MS,
4445
- backoffMaxMs: options.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS,
4446
- now: options.now ?? (() => /* @__PURE__ */ new Date())
4447
- };
4679
+ async function commitAll(params) {
4680
+ const addResult = await runGit(["add", "-A"], { cwd: params.worktreeDir });
4681
+ if (!addResult.ok) {
4682
+ return gitErr(describeGitFailure("git add -A", addResult));
4683
+ }
4684
+ const status = await getWorktreeStatus(params.worktreeDir);
4685
+ if (!status.ok) return status;
4686
+ if (status.data.clean) {
4687
+ return gitOk({ committed: false, sha: null });
4688
+ }
4689
+ const message = buildCommitMessage(params);
4690
+ const commitResult = await runGit(["commit", "-m", message], { cwd: params.worktreeDir });
4691
+ if (!commitResult.ok) {
4692
+ return gitErr(describeGitFailure("git commit", commitResult));
4693
+ }
4694
+ const sha = await getHeadSha(params.worktreeDir);
4695
+ return gitOk({ committed: true, sha: sha.ok ? sha.data : null });
4448
4696
  }
4449
- var AgentSocketClient = class extends EventEmitter {
4450
- socket;
4451
- opts;
4452
- agentId;
4453
- stopped = true;
4454
- reconnectAttempt = 0;
4455
- reconnectTimer;
4456
- heartbeatTimer;
4457
- lastHelloAck;
4458
- paused = false;
4459
- pauseReasonText;
4460
- leaseLostTasks = /* @__PURE__ */ new Map();
4461
- constructor(options) {
4462
- super();
4463
- this.opts = resolveOptions(options);
4464
- this.agentId = options.agentId;
4465
- const factory = options.createSocket ?? createSocket;
4466
- this.socket = factory(`${trimTrailingSlash2(options.url)}/agent`, {
4467
- auth: { token: options.token },
4468
- autoConnect: false,
4469
- reconnection: false
4470
- });
4471
- this.registerSocketListeners();
4472
- }
4473
- // -- Публичное API -----------------------------------------------------
4474
- on(event, listener) {
4475
- return super.on(event, listener);
4476
- }
4477
- once(event, listener) {
4478
- return super.once(event, listener);
4697
+
4698
+ // src/git/branch-name.ts
4699
+ var FORBIDDEN_CHARS = /[\s~^:?*[\\\x00-\x1f\x7f]/;
4700
+ function isValidGitBranchName(name) {
4701
+ if (name.length === 0 || name.length > 255) return false;
4702
+ if (name.startsWith("-")) return false;
4703
+ if (name.startsWith("/") || name.endsWith("/")) return false;
4704
+ if (name.endsWith(".")) return false;
4705
+ if (name.includes("..")) return false;
4706
+ if (name.includes("//")) return false;
4707
+ if (name.includes("@{")) return false;
4708
+ if (name === "@") return false;
4709
+ if (FORBIDDEN_CHARS.test(name)) return false;
4710
+ const segments = name.split("/");
4711
+ for (const segment of segments) {
4712
+ if (segment.length === 0) return false;
4713
+ if (segment.startsWith(".")) return false;
4714
+ if (segment.endsWith(".lock")) return false;
4479
4715
  }
4480
- off(event, listener) {
4481
- return super.off(event, listener);
4716
+ return true;
4717
+ }
4718
+ function requireValidBranchName(name, label) {
4719
+ if (!isValidGitBranchName(name)) {
4720
+ return gitErr(
4721
+ `${label} "${name}" \u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0438\u043C\u044F git-\u0432\u0435\u0442\u043A\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
4722
+ );
4482
4723
  }
4483
- emit(event, ...args) {
4484
- return super.emit(event, ...args);
4724
+ return gitOk(name);
4725
+ }
4726
+
4727
+ // src/git/worktree.ts
4728
+ import { createHash as createHash2 } from "crypto";
4729
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, realpathSync } from "fs";
4730
+ import { dirname as dirname4, isAbsolute, join as join9, relative, resolve as resolve2 } from "path";
4731
+ var WORKTREES_RELATIVE_ROOT = join9(".claude", "worktrees");
4732
+ function getDefaultWorktreesRoot(repoRoot) {
4733
+ return join9(repoRoot, WORKTREES_RELATIVE_ROOT);
4734
+ }
4735
+ function resolveWorktreeDir(worktreesRoot, repoRoot, branchName) {
4736
+ if (isInsideRepo(worktreesRoot, repoRoot)) return join9(worktreesRoot, branchName);
4737
+ const repoHash = createHash2("sha1").update(resolve2(repoRoot)).digest("hex").slice(0, 12);
4738
+ return join9(worktreesRoot, repoHash, branchName);
4739
+ }
4740
+ function isInsideRepo(candidate, repoRoot) {
4741
+ const rel = relative(resolve2(repoRoot), resolve2(candidate));
4742
+ return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
4743
+ }
4744
+ function canonicalPath(path) {
4745
+ try {
4746
+ return realpathSync(path);
4747
+ } catch {
4748
+ return resolve2(path);
4485
4749
  }
4486
- /** Поднимает соединение и heartbeat-таймер. Идемпотентно — повторный вызов на уже запущенном клиенте не делает ничего. */
4487
- start() {
4488
- if (!this.stopped) return;
4489
- this.stopped = false;
4490
- this.reconnectAttempt = 0;
4491
- this.startHeartbeatTimer();
4492
- this.socket.connect();
4750
+ }
4751
+ async function listWorktrees(repoRoot) {
4752
+ const result = await runGit(["worktree", "list", "--porcelain"], { cwd: repoRoot });
4753
+ if (!result.ok) {
4754
+ return gitErr(describeGitFailure("git worktree list", result));
4493
4755
  }
4494
- /** Останавливает heartbeat, отменяет запланированное переподключение, закрывает сокет. После `stop()` клиент больше не переподключается сам. */
4495
- stop() {
4496
- this.stopped = true;
4497
- if (this.reconnectTimer) {
4498
- clearTimeout(this.reconnectTimer);
4499
- this.reconnectTimer = void 0;
4756
+ const entries = [];
4757
+ let path;
4758
+ let branch = null;
4759
+ const flush = () => {
4760
+ if (path) entries.push({ path, branch });
4761
+ path = void 0;
4762
+ branch = null;
4763
+ };
4764
+ for (const line of result.stdout.split("\n")) {
4765
+ if (line.length === 0) {
4766
+ flush();
4767
+ continue;
4500
4768
  }
4501
- this.stopHeartbeatTimer();
4502
- this.socket.disconnect();
4769
+ if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
4770
+ else if (line.startsWith("branch refs/heads/")) branch = line.slice("branch refs/heads/".length);
4503
4771
  }
4504
- get connected() {
4505
- return this.socket.connected;
4772
+ flush();
4773
+ return gitOk(entries);
4774
+ }
4775
+ async function ensureWorktree(params) {
4776
+ const remote = params.remote ?? "origin";
4777
+ const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
4778
+ if (!branchCheck.ok) return branchCheck;
4779
+ const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
4780
+ if (!defaultBranchCheck.ok) return defaultBranchCheck;
4781
+ const worktreesRoot = params.worktreesRoot ?? getDefaultWorktreesRoot(params.repoRoot);
4782
+ const worktreeDir = resolveWorktreeDir(worktreesRoot, params.repoRoot, params.branchName);
4783
+ const list = await listWorktrees(params.repoRoot);
4784
+ if (!list.ok) return list;
4785
+ const existing = list.data.find((wt) => canonicalPath(wt.path) === canonicalPath(worktreeDir));
4786
+ if (existing) {
4787
+ if (existing.branch === params.branchName) {
4788
+ return gitOk({ worktreeDir, reused: true });
4789
+ }
4790
+ return gitErr(
4791
+ `\u041F\u043E \u043F\u0443\u0442\u0438 ${worktreeDir} \u0443\u0436\u0435 \u0435\u0441\u0442\u044C git worktree \u043D\u0430 \u0432\u0435\u0442\u043A\u0435 "${existing.branch ?? "(\u043E\u0442\u0441\u043E\u0435\u0434\u0438\u043D\u0451\u043D\u043D\u044B\u0439 HEAD)"}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}". \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0440\u0443\u043A\u0430\u043C\u0438: "git -C ${params.repoRoot} worktree remove ${worktreeDir}" (\u0434\u043E\u0431\u0430\u0432\u044C "--force", \u0435\u0441\u043B\u0438 \u0442\u0430\u043C \u043D\u0435\u0442 \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u0430\u0436\u043D\u043E\u0433\u043E), \u0437\u0430\u0442\u0435\u043C \u043F\u043E\u0432\u0442\u043E\u0440\u0438.`
4792
+ );
4506
4793
  }
4507
- get lastHello() {
4508
- return this.lastHelloAck;
4794
+ if (existsSync9(worktreeDir)) {
4795
+ return gitErr(
4796
+ `\u041F\u0443\u0442\u044C ${worktreeDir} \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E git \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0435\u0433\u043E \u0441\u0432\u043E\u0435\u0439 worktree. \u0423\u0431\u0435\u0440\u0438 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u2014 \u0442\u0440\u043E\u0433\u0430\u0442\u044C \u0447\u0443\u0436\u043E\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E.`
4797
+ );
4509
4798
  }
4510
- get isPaused() {
4511
- return this.paused;
4799
+ await runGit(["worktree", "prune"], { cwd: params.repoRoot });
4800
+ const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
4801
+ cwd: params.repoRoot
4802
+ });
4803
+ if (!fetchResult.ok) {
4804
+ return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
4512
4805
  }
4513
- get pauseReason() {
4514
- return this.pauseReasonText;
4806
+ let startPoint = `${remote}/${params.defaultBranch}`;
4807
+ if (params.continueFromBranch) {
4808
+ const continued = await runGit(["fetch", remote, params.continueFromBranch], {
4809
+ cwd: params.repoRoot
4810
+ });
4811
+ if (continued.ok) startPoint = `${remote}/${params.continueFromBranch}`;
4515
4812
  }
4516
- /** §11.2 ТЗ: «следующий вызов любого тула по этой задаче должен сообщить модели, что работу надо прекратить» — тулы (P2-12) сверяются с этим геттером. */
4517
- isLeaseLost(taskId) {
4518
- const reason = this.leaseLostTasks.get(taskId);
4519
- return reason === void 0 ? { lost: false } : { lost: true, reason };
4813
+ mkdirSync6(dirname4(worktreeDir), { recursive: true });
4814
+ const branchExists = await localBranchExists(params.repoRoot, params.branchName);
4815
+ const addArgs = branchExists ? ["worktree", "add", worktreeDir, params.branchName] : ["worktree", "add", "-b", params.branchName, worktreeDir, startPoint];
4816
+ const addResult = await runGit(addArgs, { cwd: params.repoRoot });
4817
+ if (!addResult.ok) {
4818
+ return gitErr(describeGitFailure("git worktree add", addResult));
4520
4819
  }
4521
- /**
4522
- * Общий emit+ack поверх ЭТОГО ЖЕ постоянного сокета — тем же приёмом
4523
- * (`emitWithAckTimeout`), которым `sendHello`/`sendHeartbeat` уже
4524
- * пользуются внутри класса, но публично: MCP-тулы (P2-12/13,
4525
- * `../mcp/**`) шлют через него `task:claim`/`task:start`/`task:progress`/
4526
- * `task:get`/`task:report`/`task:release`/`context:get` (§11.1 ТЗ) — то
4527
- * есть ВСЁ, что не `agent:hello`/`agent:heartbeat`/`agent:mode` (у
4528
- * первых двух своя логика согласования состояния в этом классе, третий
4529
- * — короткоживущий отдельный сокет, см. `send-agent-mode.ts`). Нарочно
4530
- * НЕ отдельное соединение: `task:claim` — блокирующий долгий long-poll,
4531
- * и держать его на том же сокете, где уже идёт heartbeat/presence этого
4532
- * агента, а не поднимать вторую параллельную сессию — единственный
4533
- * вариант, не удваивающий presence на сервере.
4534
- */
4535
- callServer(event, payload, timeoutMs = this.opts.ackTimeoutMs) {
4536
- return emitWithAckTimeout(this.socket, event, payload, timeoutMs);
4820
+ return gitOk({ worktreeDir, reused: false });
4821
+ }
4822
+ async function removeWorktree(repoRoot, worktreeDir) {
4823
+ const result = await runGit(["worktree", "remove", worktreeDir], { cwd: repoRoot });
4824
+ if (!result.ok) {
4825
+ return gitErr(describeGitFailure("git worktree remove", result));
4537
4826
  }
4538
- /**
4539
- * Ждёт первого успешного `agent:hello` не дольше `timeoutMs` (по
4540
- * умолчанию 5 с — как раньше HTTP-пинг в `network.ts`). Таймаут НЕ
4541
- * останавливает клиент: переподключение по backoff продолжается в фоне,
4542
- * это только ограничение по времени для вызывающей стороны (`connect.ts`).
4543
- */
4544
- waitForFirstConnection(timeoutMs = 5e3) {
4545
- return new Promise((resolve4) => {
4546
- let settled = false;
4547
- const onConnected = (ack) => {
4548
- if (settled) return;
4549
- settled = true;
4550
- clearTimeout(timer);
4551
- this.off("connected", onConnected);
4552
- resolve4({ ok: true, ack });
4553
- };
4554
- const onAuthRejected = (message) => {
4555
- if (settled) return;
4556
- settled = true;
4557
- clearTimeout(timer);
4558
- this.off("connected", onConnected);
4559
- this.off("auth-rejected", onAuthRejected);
4560
- resolve4({ ok: false, message });
4561
- };
4562
- const timer = setTimeout(() => {
4563
- if (settled) return;
4564
- settled = true;
4565
- this.off("connected", onConnected);
4566
- this.off("auth-rejected", onAuthRejected);
4567
- resolve4({
4568
- ok: false,
4569
- message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 agent:hello \u0437\u0430 ${timeoutMs} \u043C\u0441 \u2014 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430. \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0432 \u0444\u043E\u043D\u0435 (backoff \xA711.4 \u0422\u0417).`
4570
- });
4571
- }, timeoutMs);
4572
- if (typeof timer.unref === "function") timer.unref();
4573
- this.on("connected", onConnected);
4574
- this.on("auth-rejected", onAuthRejected);
4575
- });
4827
+ return gitOk(void 0);
4828
+ }
4829
+
4830
+ // src/git/submit.ts
4831
+ async function rebaseOntoDefault(params) {
4832
+ const remote = params.remote ?? "origin";
4833
+ const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
4834
+ if (!defaultBranchCheck.ok) return defaultBranchCheck;
4835
+ const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
4836
+ cwd: params.worktreeDir
4837
+ });
4838
+ if (!fetchResult.ok) {
4839
+ return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
4576
4840
  }
4577
- // -- Подключение --------------------------------------------------------
4578
- registerSocketListeners() {
4579
- this.socket.on("connect", () => this.handleConnect());
4580
- this.socket.on("connect_error", (err) => this.handleConnectError(err));
4581
- this.socket.on("disconnect", (reason) => this.handleDisconnect(String(reason)));
4582
- this.socket.on("lease:lost", (raw) => this.handleLeaseLost(raw));
4583
- this.socket.on("mode:set", (raw) => this.handleModeSet(raw));
4584
- this.socket.on("agent:pause", (raw) => this.handleAgentPause(raw));
4585
- this.socket.on("trust:updated", (raw) => this.handleTrustUpdated(raw));
4586
- this.socket.on("task:updated", (raw) => this.handleTaskUpdated(raw));
4587
- this.socket.on("question:answered", (raw) => this.handleQuestionAnswered(raw));
4588
- this.socket.on("approval:decided", (raw) => this.handleApprovalDecided(raw));
4841
+ const rebaseResult = await runGit(["rebase", `${remote}/${params.defaultBranch}`], {
4842
+ cwd: params.worktreeDir
4843
+ });
4844
+ if (rebaseResult.ok) {
4845
+ return gitOk({ outcome: "rebased", conflictedFiles: [] });
4589
4846
  }
4590
- handleConnect() {
4591
- if (this.stopped) return;
4592
- this.reconnectAttempt = 0;
4593
- void this.sendHello();
4594
- }
4595
- handleConnectError(err) {
4596
- if (isAuthRejection(err)) {
4597
- this.stop();
4598
- this.emit("auth-rejected", describeAuthRejection(this.agentId));
4599
- return;
4600
- }
4601
- this.emit("client-error", toError2(err, "connect_error"));
4602
- this.scheduleReconnect();
4603
- }
4604
- handleDisconnect(reason) {
4605
- this.emit("disconnected", reason);
4606
- if (this.stopped) return;
4607
- this.scheduleReconnect();
4608
- }
4609
- scheduleReconnect() {
4610
- if (this.stopped || this.reconnectTimer) return;
4611
- const delayMs = nextReconnectDelayMs(this.reconnectAttempt, {
4612
- baseMs: this.opts.backoffBaseMs,
4613
- maxMs: this.opts.backoffMaxMs
4614
- });
4615
- this.reconnectAttempt += 1;
4616
- this.emit("reconnecting", { attempt: this.reconnectAttempt, delayMs });
4617
- this.reconnectTimer = setTimeout(() => {
4618
- this.reconnectTimer = void 0;
4619
- if (this.stopped) return;
4620
- this.socket.connect();
4621
- }, delayMs);
4622
- }
4623
- // -- agent:hello ---------------------------------------------------------
4624
- async sendHello() {
4625
- const state = readState(this.opts.stateFilePath);
4626
- const payload = agentHelloPayloadSchema.parse({
4627
- clientVersion: this.opts.clientVersion,
4628
- cwd: this.opts.cwd,
4629
- os: this.opts.os,
4630
- agentKind: this.opts.agentKind,
4631
- agentVersion: this.opts.agentVersion,
4632
- // `claudeVersion` шлётся ТОЛЬКО для Claude Code — и только чтобы
4633
- // клиент работал против ещё не обновлённого сервера, который знает
4634
- // одно это поле. Для Codex поле с таким именем было бы неправдой.
4635
- ...this.opts.agentKind === AgentKind.CLAUDE_CODE ? { claudeVersion: this.opts.agentVersion } : {},
4636
- roleKeys: [...this.opts.roleKeys],
4637
- ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {}
4638
- });
4639
- let raw;
4640
- try {
4641
- raw = await emitWithAckTimeout(this.socket, "agent:hello", payload, this.opts.ackTimeoutMs);
4642
- } catch (err) {
4643
- this.emit("client-error", toError2(err, "agent:hello"));
4644
- return;
4645
- }
4646
- if (this.stopped) return;
4647
- if (isAckError(raw)) {
4648
- this.emit("client-error", new Error(`agent:hello: ${raw.error.code}: ${raw.error.message}`));
4649
- return;
4650
- }
4651
- const parsed = agentHelloAckSchema.safeParse(raw);
4652
- if (!parsed.success) {
4653
- this.emit(
4654
- "client-error",
4655
- new Error(`agent:hello: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
4847
+ const status = await getWorktreeStatus(params.worktreeDir);
4848
+ if (status.ok && status.data.conflictedPaths.length > 0) {
4849
+ const conflictedFiles = status.data.conflictedPaths;
4850
+ const abortResult = await runGit(["rebase", "--abort"], { cwd: params.worktreeDir });
4851
+ if (!abortResult.ok) {
4852
+ return gitErr(
4853
+ `\u041A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043F\u0440\u0438 rebase (${conflictedFiles.join(", ")}), \u0438 "git rebase --abort" \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u043B: ${describeGitFailure("git rebase --abort", abortResult)}. Worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u043D\u043E \u0440\u0443\u0447\u043D\u043E\u0435 \u0432\u043C\u0435\u0448\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E.`
4656
4854
  );
4657
- return;
4658
4855
  }
4659
- const ack = parsed.data;
4660
- this.lastHelloAck = ack;
4661
- this.paused = false;
4662
- this.pauseReasonText = void 0;
4663
- this.applyHelloAck(ack);
4664
- this.emit("connected", ack);
4856
+ return gitOk({ outcome: "conflict", conflictedFiles });
4665
4857
  }
4666
- applyHelloAck(ack) {
4667
- const state = readState(this.opts.stateFilePath);
4668
- const patch = {
4669
- // OQ-06: сервер — источник правды, локальный режим перекрывается безусловно.
4670
- mode: ack.mode,
4671
- trust: toTrustCache(ack.trust, this.opts.now)
4672
- };
4673
- if (!ack.currentTask || ack.currentTask.id !== state.currentTask?.taskId) {
4674
- patch.currentTask = null;
4675
- }
4676
- updateState(patch, this.opts.stateFilePath);
4858
+ return gitErr(describeGitFailure("git rebase", rebaseResult));
4859
+ }
4860
+ async function assertReadyToPush(params) {
4861
+ const branch = await currentBranch(params.worktreeDir);
4862
+ if (!branch.ok) return branch;
4863
+ if (branch.data !== params.branchName) {
4864
+ return gitErr(
4865
+ `\u041E\u0442\u043A\u0430\u0437: worktree \u0441\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u0432\u0435\u0442\u043A\u0443 "${branch.data}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}" \u2014 \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E "\u0443\u0448\u043B\u043E \u043D\u0435 \u0442\u0443\u0434\u0430", push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
4866
+ );
4677
4867
  }
4678
- // -- agent:heartbeat -------------------------------------------------------
4679
- startHeartbeatTimer() {
4680
- if (this.heartbeatTimer) return;
4681
- this.heartbeatTimer = setInterval(() => {
4682
- void this.sendHeartbeat();
4683
- }, this.opts.heartbeatIntervalMs);
4868
+ const status = await getWorktreeStatus(params.worktreeDir);
4869
+ if (!status.ok) return status;
4870
+ if (!status.data.clean) {
4871
+ return gitErr(
4872
+ `\u041E\u0442\u043A\u0430\u0437: \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E \u0433\u0440\u044F\u0437\u043D\u043E\u0435, \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u043F\u0443\u0442\u0438: ${status.data.changedPaths.join(", ")}. Push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
4873
+ );
4684
4874
  }
4685
- stopHeartbeatTimer() {
4686
- if (!this.heartbeatTimer) return;
4687
- clearInterval(this.heartbeatTimer);
4688
- this.heartbeatTimer = void 0;
4875
+ return gitOk(void 0);
4876
+ }
4877
+ async function pushFastForward(params) {
4878
+ const remote = params.remote ?? "origin";
4879
+ const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
4880
+ if (!branchCheck.ok) return branchCheck;
4881
+ const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
4882
+ if (!defaultBranchCheck.ok) return defaultBranchCheck;
4883
+ const ready = await assertReadyToPush(params);
4884
+ if (!ready.ok) return ready;
4885
+ const pushResult = await runGit(["push", remote, `HEAD:refs/heads/${params.defaultBranch}`], {
4886
+ cwd: params.worktreeDir
4887
+ });
4888
+ if (!pushResult.ok) {
4889
+ return gitErr(
4890
+ describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.defaultBranch}`, pushResult)
4891
+ );
4689
4892
  }
4690
- async sendHeartbeat() {
4691
- if (!this.socket.connected) return;
4692
- const state = readState(this.opts.stateFilePath);
4693
- const payload = agentHeartbeatPayloadSchema.parse({
4694
- ...state.currentTask?.taskId ? { currentTaskId: state.currentTask.taskId } : {},
4695
- ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {},
4696
- status: state.currentTask ? AgentStatus.BUSY : AgentStatus.IDLE
4893
+ return gitOk({ pushedTo: `${remote}/${params.defaultBranch}` });
4894
+ }
4895
+ async function submitDirectTask(params) {
4896
+ const remote = params.remote ?? "origin";
4897
+ let worktreeDir;
4898
+ if (params.worktreeDir) {
4899
+ worktreeDir = params.worktreeDir;
4900
+ } else {
4901
+ const worktree = await ensureWorktree({
4902
+ repoRoot: params.repoRoot,
4903
+ branchName: params.branchName,
4904
+ defaultBranch: params.defaultBranch,
4905
+ remote,
4906
+ worktreesRoot: params.worktreesRoot
4697
4907
  });
4698
- let raw;
4699
- try {
4700
- raw = await emitWithAckTimeout(
4701
- this.socket,
4702
- "agent:heartbeat",
4703
- payload,
4704
- this.opts.ackTimeoutMs
4705
- );
4706
- } catch (err) {
4707
- this.emit("client-error", toError2(err, "agent:heartbeat"));
4708
- return;
4709
- }
4710
- if (isAckError(raw)) {
4711
- this.emit(
4712
- "client-error",
4713
- new Error(`agent:heartbeat: ${raw.error.code}: ${raw.error.message}`)
4714
- );
4715
- return;
4716
- }
4717
- const parsed = agentHeartbeatAckSchema.safeParse(raw);
4718
- if (!parsed.success) {
4719
- this.emit(
4720
- "client-error",
4721
- new Error(`agent:heartbeat: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
4722
- );
4723
- return;
4724
- }
4725
- this.emit("heartbeat", parsed.data);
4908
+ if (!worktree.ok) return { status: "refused", message: worktree.message };
4909
+ worktreeDir = worktree.data.worktreeDir;
4726
4910
  }
4727
- // -- Пуш-события сервера (§11.2 ТЗ) --------------------------------------
4728
- handleLeaseLost(raw) {
4729
- const parsed = leaseLostPayloadSchema.safeParse(raw);
4730
- if (!parsed.success) {
4731
- this.emit(
4732
- "client-error",
4733
- new Error(`lease:lost: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4734
- );
4735
- return;
4736
- }
4737
- const { taskId, reason } = parsed.data;
4738
- this.leaseLostTasks.set(taskId, reason);
4739
- const state = readState(this.opts.stateFilePath);
4740
- if (state.currentTask?.taskId === taskId) {
4741
- updateState({ currentTask: null }, this.opts.stateFilePath);
4742
- }
4743
- this.emit("lease:lost", parsed.data);
4911
+ const commit = await commitAll({
4912
+ worktreeDir,
4913
+ commitPrefix: params.commitPrefix,
4914
+ title: params.taskTitle,
4915
+ taskKey: params.taskKey
4916
+ });
4917
+ if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
4918
+ const rebase = await rebaseOntoDefault({
4919
+ worktreeDir,
4920
+ defaultBranch: params.defaultBranch,
4921
+ remote
4922
+ });
4923
+ if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
4924
+ if (rebase.data.outcome === "conflict") {
4925
+ const files = rebase.data.conflictedFiles;
4926
+ return {
4927
+ status: "conflict",
4928
+ worktreeDir,
4929
+ conflictedFiles: files,
4930
+ message: `Rebase \u043D\u0430 ${remote}/${params.defaultBranch} \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0432 \u0444\u0430\u0439\u043B\u0430\u0445: ${files.join(", ")}. Rebase \u043E\u0442\u043C\u0435\u043D\u0451\u043D ("git rebase --abort"), \u0432\u0435\u0442\u043A\u0430 "${params.branchName}" \u0438 \u0435\u0451 worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`
4931
+ };
4744
4932
  }
4745
- handleModeSet(raw) {
4746
- const parsed = modeSetPayloadSchema.safeParse(raw);
4747
- if (!parsed.success) {
4748
- this.emit("client-error", new Error(`mode:set: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`));
4749
- return;
4933
+ if (params.requireTestsPassed && !params.testsPassed) {
4934
+ return {
4935
+ status: "refused",
4936
+ worktreeDir,
4937
+ message: "\u0414\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 requireTests=true, \u0430 testsPassed \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D (\u0438\u043B\u0438 false) \u2014 push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F."
4938
+ };
4939
+ }
4940
+ const push = await pushFastForward({
4941
+ worktreeDir,
4942
+ branchName: params.branchName,
4943
+ defaultBranch: params.defaultBranch,
4944
+ remote
4945
+ });
4946
+ if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
4947
+ const sha = await getHeadSha(worktreeDir);
4948
+ let worktreeRemoved = false;
4949
+ if (params.removeWorktreeOnSuccess !== false) {
4950
+ const removed = await removeWorktree(params.repoRoot, worktreeDir);
4951
+ if (removed.ok) {
4952
+ await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
4953
+ worktreeRemoved = true;
4750
4954
  }
4751
- updateState({ mode: parsed.data.mode }, this.opts.stateFilePath);
4752
- this.emit("mode:set", parsed.data);
4753
4955
  }
4754
- handleAgentPause(raw) {
4755
- const parsed = agentPausePayloadSchema.safeParse(raw);
4756
- if (!parsed.success) {
4757
- this.emit(
4758
- "client-error",
4759
- new Error(`agent:pause: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4760
- );
4761
- return;
4762
- }
4763
- this.paused = true;
4764
- this.pauseReasonText = parsed.data.reason;
4765
- this.emit("pause", parsed.data);
4956
+ return {
4957
+ status: "pushed",
4958
+ worktreeDir,
4959
+ commitSha: sha.ok ? sha.data : null,
4960
+ pushedTo: push.data.pushedTo,
4961
+ worktreeRemoved
4962
+ };
4963
+ }
4964
+
4965
+ // src/git/submit-pr.ts
4966
+ async function pushTaskBranch(params) {
4967
+ const remote = params.remote ?? "origin";
4968
+ const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
4969
+ if (!branchCheck.ok) return branchCheck;
4970
+ const ready = await assertReadyToPush(params);
4971
+ if (!ready.ok) return ready;
4972
+ const pushResult = await runGit(
4973
+ ["push", "--set-upstream", remote, `HEAD:refs/heads/${params.branchName}`],
4974
+ { cwd: params.worktreeDir }
4975
+ );
4976
+ if (!pushResult.ok) {
4977
+ return gitErr(
4978
+ describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.branchName}`, pushResult)
4979
+ );
4766
4980
  }
4767
- handleTrustUpdated(raw) {
4768
- const parsed = trustUpdatedPayloadSchema.safeParse(raw);
4769
- if (!parsed.success) {
4770
- this.emit(
4771
- "client-error",
4772
- new Error(`trust:updated: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4773
- );
4774
- return;
4981
+ return gitOk({ pushedTo: `${remote}/${params.branchName}` });
4982
+ }
4983
+ async function submitPullRequestTask(params) {
4984
+ const remote = params.remote ?? "origin";
4985
+ let worktreeDir;
4986
+ if (params.worktreeDir) {
4987
+ worktreeDir = params.worktreeDir;
4988
+ } else {
4989
+ const worktree = await ensureWorktree({
4990
+ repoRoot: params.repoRoot,
4991
+ branchName: params.branchName,
4992
+ defaultBranch: params.defaultBranch,
4993
+ remote,
4994
+ worktreesRoot: params.worktreesRoot
4995
+ });
4996
+ if (!worktree.ok) return { status: "refused", message: worktree.message };
4997
+ worktreeDir = worktree.data.worktreeDir;
4998
+ }
4999
+ const commit = await commitAll({
5000
+ worktreeDir,
5001
+ commitPrefix: params.commitPrefix,
5002
+ title: params.taskTitle,
5003
+ taskKey: params.taskKey
5004
+ });
5005
+ if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
5006
+ const rebase = await rebaseOntoDefault({
5007
+ worktreeDir,
5008
+ defaultBranch: params.defaultBranch,
5009
+ remote
5010
+ });
5011
+ if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
5012
+ const conflicted = rebase.data.outcome === "conflict";
5013
+ const push = await pushTaskBranch({ worktreeDir, branchName: params.branchName, remote });
5014
+ if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
5015
+ const sha = await getHeadSha(worktreeDir);
5016
+ let worktreeRemoved = false;
5017
+ if (params.removeWorktreeOnSuccess !== false) {
5018
+ const removed = await removeWorktree(params.repoRoot, worktreeDir);
5019
+ if (removed.ok) {
5020
+ await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
5021
+ worktreeRemoved = true;
4775
5022
  }
4776
- updateState({ trust: toTrustCache(parsed.data, this.opts.now) }, this.opts.stateFilePath);
4777
- this.emit("trust:updated", parsed.data);
4778
5023
  }
4779
- handleTaskUpdated(raw) {
4780
- const parsed = taskUpdatedAgentPayloadSchema.safeParse(raw);
4781
- if (parsed.success) this.emit("task:updated", parsed.data);
5024
+ return {
5025
+ status: "pushed",
5026
+ worktreeDir,
5027
+ branchName: params.branchName,
5028
+ commitSha: sha.ok ? sha.data : null,
5029
+ pushedTo: push.data.pushedTo,
5030
+ conflicted,
5031
+ conflictedFiles: rebase.data.conflictedFiles,
5032
+ worktreeRemoved
5033
+ };
5034
+ }
5035
+
5036
+ // src/git/rescue.ts
5037
+ var RescueReason = {
5038
+ /** Модель упёрлась в лимит подписки (`StopFailure`, matcher `rate_limit`). */
5039
+ RATE_LIMIT: "rate_limit",
5040
+ /** Сервер забрал задачу: аренда истекла или её отдали другому. */
5041
+ LEASE_LOST: "lease_lost"
5042
+ };
5043
+ var REASON_TEXT = {
5044
+ [RescueReason.RATE_LIMIT]: "\u043F\u0440\u0435\u0440\u0432\u0430\u043D\u043E: \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u043B\u0438\u043C\u0438\u0442 \u043F\u043E\u0434\u043F\u0438\u0441\u043A\u0438",
5045
+ [RescueReason.LEASE_LOST]: "\u043F\u0440\u0435\u0440\u0432\u0430\u043D\u043E: \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0442\u043E\u0437\u0432\u0430\u043D\u0430 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u043E\u0439"
5046
+ };
5047
+ async function rescueUnfinishedWork(params) {
5048
+ const status = await getWorktreeStatus(params.worktreeDir);
5049
+ if (!status.ok) return status;
5050
+ if (status.data.clean) {
5051
+ return gitOk({ committed: false, sha: null, pushedTo: null });
4782
5052
  }
4783
- handleQuestionAnswered(raw) {
4784
- const parsed = questionAnsweredPayloadSchema.safeParse(raw);
4785
- if (parsed.success) this.emit("question:answered", parsed.data);
5053
+ const what = params.title?.trim();
5054
+ const commit = await commitAll({
5055
+ worktreeDir: params.worktreeDir,
5056
+ commitPrefix: params.commitPrefix,
5057
+ title: what ? `${what} (${REASON_TEXT[params.reason]})` : REASON_TEXT[params.reason],
5058
+ taskKey: params.taskKey
5059
+ });
5060
+ if (!commit.ok) return commit;
5061
+ if (!commit.data.committed) {
5062
+ return gitOk({ committed: false, sha: null, pushedTo: null });
4786
5063
  }
4787
- /**
4788
- * §14.4 ТЗ, п.4-5 — асинхронный путь (в дополнение к синхронному в
4789
- * `mcp/request-approval.ts::settleApproval`, который срабатывает, когда
4790
- * решение приходит в рамках ОДНОГО вызова `request_approval`/
4791
- * `wait_approval`). Этот пуш ловит случай, когда решение пришло уже
4792
- * ПОСЛЕ того, как собственный long-poll тула истёк (вернул `PENDING`),
4793
- * а модель ещё не успела/не стала звать `wait_approval` снова — на
4794
- * постоянном соединении (`quiel mcp`, единственный держатель ЭТОГО
4795
- * класса) `approval:decided` долетит и без явного повторного вызова.
4796
- *
4797
- * `approvalDecidedPayloadSchema` несёт только `approvalId`+`status`+
4798
- * `oneTimeToken?` — НЕ `command`/`cwd` (провод их не отдаёт, см.
4799
- * `state/schema.ts::PendingApprovalSchema`), поэтому command+cwd берутся
4800
- * из `state.pendingApprovals[approvalId]`, куда их кладёт
4801
- * `request_approval` сразу после создания запроса. Если записи там нет
4802
- * (approval создан не этим клиентом/до перезапуска процесса, либо уже
4803
- * обработан) — писать пока нечего, `pendingApprovals` и так пуст.
4804
- */
4805
- handleApprovalDecided(raw) {
4806
- const parsed = approvalDecidedPayloadSchema.safeParse(raw);
4807
- if (!parsed.success) {
4808
- this.emit(
4809
- "client-error",
4810
- new Error(`approval:decided: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
4811
- );
4812
- return;
5064
+ const push = await pushTaskBranch({
5065
+ worktreeDir: params.worktreeDir,
5066
+ branchName: params.branchName
5067
+ });
5068
+ if (!push.ok) {
5069
+ return gitErr(
5070
+ `\u0420\u0430\u0431\u043E\u0442\u0430 \u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u0430 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E (${commit.data.sha ?? "\u0431\u0435\u0437 sha"}), \u043D\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0432\u0435\u0442\u043A\u0443 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${push.message}`
5071
+ );
5072
+ }
5073
+ return gitOk({
5074
+ committed: true,
5075
+ sha: commit.data.sha,
5076
+ pushedTo: push.data.pushedTo
5077
+ });
5078
+ }
5079
+
5080
+ // src/hooks/stop-failure.ts
5081
+ import { z as z37 } from "zod";
5082
+
5083
+ // src/hooks/reset-time.ts
5084
+ var DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
5085
+ function parseClock(hour, minute, meridiem) {
5086
+ const lower = meridiem.toLowerCase();
5087
+ let h = hour % 12;
5088
+ if (lower === "pm") h += 12;
5089
+ return { h, m: minute };
5090
+ }
5091
+ function parseResetAt(message, now) {
5092
+ if (!message) return void 0;
5093
+ const withDay = message.match(
5094
+ /resets\s+(sun|mon|tue|wed|thu|fri|sat)[a-z]*\s+(\d{1,2}):(\d{2})\s*(am|pm)/i
5095
+ );
5096
+ if (withDay) {
5097
+ const [, dayRaw = "", hourRaw = "", minuteRaw = "", meridiem = ""] = withDay;
5098
+ const targetDay = DAYS.indexOf(dayRaw.slice(0, 3).toLowerCase());
5099
+ if (targetDay === -1) return void 0;
5100
+ const { h, m } = parseClock(Number(hourRaw), Number(minuteRaw), meridiem);
5101
+ const result = new Date(now);
5102
+ result.setHours(h, m, 0, 0);
5103
+ let shift = (targetDay - result.getDay() + 7) % 7;
5104
+ if (shift === 0 && result.getTime() <= now.getTime()) shift = 7;
5105
+ result.setDate(result.getDate() + shift);
5106
+ return result;
5107
+ }
5108
+ const withTime = message.match(/resets\s+(\d{1,2}):(\d{2})\s*(am|pm)/i);
5109
+ if (withTime) {
5110
+ const [, hourRaw = "", minuteRaw = "", meridiem = ""] = withTime;
5111
+ const { h, m } = parseClock(Number(hourRaw), Number(minuteRaw), meridiem);
5112
+ const result = new Date(now);
5113
+ result.setHours(h, m, 0, 0);
5114
+ if (result.getTime() <= now.getTime()) result.setDate(result.getDate() + 1);
5115
+ return result;
5116
+ }
5117
+ return void 0;
5118
+ }
5119
+
5120
+ // src/hooks/stop-failure.ts
5121
+ var StopFailureErrorTypeSchema = z37.enum([
5122
+ "rate_limit",
5123
+ "overloaded",
5124
+ "authentication_failed",
5125
+ "oauth_org_not_allowed",
5126
+ "account_on_hold",
5127
+ "billing_error",
5128
+ "invalid_request",
5129
+ "model_not_found",
5130
+ "server_error",
5131
+ "max_output_tokens",
5132
+ // Добавлен 21 сентября 2026 по живой документации: его в нашем
5133
+ // перечислении не было, и `StopFailureInputSchema` отклонила бы вход с
5134
+ // ним целиком — хук отработал бы вхолостую, а сервер не узнал бы, что
5135
+ // у агента что-то случилось.
5136
+ "cloud_credential_error",
5137
+ "unknown"
5138
+ ]);
5139
+ var StopFailureInputSchema = z37.object({
5140
+ hook_event_name: z37.literal("StopFailure"),
5141
+ session_id: z37.string(),
5142
+ cwd: z37.string(),
5143
+ error: StopFailureErrorTypeSchema,
5144
+ error_details: z37.unknown().optional(),
5145
+ last_assistant_message: z37.string().optional()
5146
+ }).passthrough();
5147
+ function extractRetryAfterSec(errorDetails) {
5148
+ if (typeof errorDetails === "number" && Number.isFinite(errorDetails) && errorDetails >= 0) {
5149
+ return errorDetails;
5150
+ }
5151
+ if (errorDetails && typeof errorDetails === "object") {
5152
+ const record = errorDetails;
5153
+ for (const key of ["retryAfterSec", "retryAfter", "retry_after", "retry_after_seconds"]) {
5154
+ const value = record[key];
5155
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
4813
5156
  }
4814
- this.applyApprovalDecided(parsed.data);
4815
- this.emit("approval:decided", parsed.data);
4816
5157
  }
4817
- applyApprovalDecided(payload) {
4818
- const state = readState(this.opts.stateFilePath);
4819
- const pending = state.pendingApprovals[payload.approvalId];
4820
- if (!pending) return;
4821
- if (payload.status === ApprovalStatus.APPROVED && payload.oneTimeToken) {
4822
- const approvalTtlMin = state.trust?.rules.approvalTtlMin ?? 60;
4823
- const expiresAt = new Date(this.opts.now().getTime() + approvalTtlMin * 6e4).toISOString();
4824
- updateState(
4825
- {
4826
- ...storeApprovedCommand(
4827
- state,
4828
- { command: pending.command, cwd: pending.cwd, token: payload.oneTimeToken, expiresAt },
4829
- this.opts.now
4830
- ),
4831
- ...clearPendingApproval(state, payload.approvalId)
4832
- },
4833
- this.opts.stateFilePath
4834
- );
4835
- return;
5158
+ if (typeof errorDetails === "string") {
5159
+ const match = errorDetails.match(/retry[-_ ]?after[^0-9]{0,10}(\d+)/i);
5160
+ if (match?.[1] !== void 0) return Number(match[1]);
5161
+ }
5162
+ return void 0;
5163
+ }
5164
+ var RESCUE_WORK_ACTION = "rescue-work";
5165
+ function stopFailureHook(input, state) {
5166
+ const retryAfterSec = extractRetryAfterSec(input.error_details);
5167
+ const resetAt = parseResetAt(input.last_assistant_message, /* @__PURE__ */ new Date());
5168
+ const actions = [
5169
+ {
5170
+ type: "agent:rate_limited",
5171
+ payload: {
5172
+ error: input.error,
5173
+ ...retryAfterSec === void 0 ? {} : { retryAfterSec },
5174
+ ...resetAt ? { resetsAt: resetAt.toISOString() } : {}
5175
+ }
4836
5176
  }
4837
- updateState(clearPendingApproval(state, payload.approvalId), this.opts.stateFilePath);
5177
+ ];
5178
+ const task = state.currentTask;
5179
+ if (task?.worktreeDir && task.branchName && task.commitPrefix) {
5180
+ actions.push({
5181
+ type: RESCUE_WORK_ACTION,
5182
+ payload: {
5183
+ reason: "rate_limit",
5184
+ taskId: task.taskId,
5185
+ worktreeDir: task.worktreeDir,
5186
+ branchName: task.branchName,
5187
+ commitPrefix: task.commitPrefix,
5188
+ taskKey: task.taskKey ?? task.taskId
5189
+ }
5190
+ });
4838
5191
  }
4839
- };
4840
- function trimTrailingSlash2(url) {
4841
- return url.endsWith("/") ? url.slice(0, -1) : url;
5192
+ return { output: {}, serverActions: actions };
4842
5193
  }
4843
5194
 
4844
- // src/socket/send-agent-mode.ts
4845
- var DEFAULT_TIMEOUT_MS = 8e3;
4846
- function trimTrailingSlash3(url) {
4847
- return url.endsWith("/") ? url.slice(0, -1) : url;
4848
- }
4849
- async function sendAgentMode(options) {
4850
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4851
- const factory = options.createSocket ?? createSocket;
4852
- const socket = factory(`${trimTrailingSlash3(options.url)}/agent`, {
4853
- auth: { token: options.token },
4854
- autoConnect: false,
4855
- reconnection: false
4856
- });
5195
+ // src/hooks/rescue-runner.ts
5196
+ var NOTHING = {
5197
+ attempted: false,
5198
+ committed: false,
5199
+ pushedTo: null,
5200
+ problem: null,
5201
+ report: null
5202
+ };
5203
+ function readParams(action) {
5204
+ const p = action.payload ?? {};
5205
+ const str = (key) => {
5206
+ const value = p[key];
5207
+ return typeof value === "string" && value.length > 0 ? value : null;
5208
+ };
5209
+ const worktreeDir = str("worktreeDir");
5210
+ const branchName = str("branchName");
5211
+ const commitPrefix = str("commitPrefix");
5212
+ const taskKey = str("taskKey");
5213
+ const taskId = str("taskId");
5214
+ if (!worktreeDir || !branchName || !commitPrefix || !taskKey || !taskId) return null;
5215
+ const reason = p.reason === RescueReason.LEASE_LOST ? RescueReason.LEASE_LOST : RescueReason.RATE_LIMIT;
5216
+ return { worktreeDir, branchName, commitPrefix, taskKey, taskId, reason };
5217
+ }
5218
+ async function runRescueAction(actions) {
5219
+ const action = actions.find((candidate) => candidate.type === RESCUE_WORK_ACTION);
5220
+ if (!action) return NOTHING;
5221
+ const params = readParams(action);
5222
+ if (!params) {
5223
+ return { ...NOTHING, attempted: true, problem: "\u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u043E \u0437\u0430\u0434\u0430\u0447\u0435" };
5224
+ }
4857
5225
  try {
4858
- await connectOnce(socket, timeoutMs);
4859
- const payload = agentModePayloadSchema.parse({ mode: options.mode });
4860
- const raw = await emitWithAckTimeout(socket, "agent:mode", payload, timeoutMs);
4861
- if (isAckError(raw)) {
4862
- return { ok: false, message: `${raw.error.code}: ${raw.error.message}` };
4863
- }
4864
- const parsed = agentModeAckSchema.safeParse(raw);
4865
- if (!parsed.success) {
4866
- return { ok: false, message: `\u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}` };
5226
+ const result = await rescueUnfinishedWork(params);
5227
+ if (!result.ok) {
5228
+ return { ...NOTHING, attempted: true, problem: result.message };
4867
5229
  }
4868
- if (!parsed.data.ok) {
4869
- return { ok: false, message: "\u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B agent:mode (ok:false)" };
4870
- }
4871
- return { ok: true, mode: parsed.data.mode ?? options.mode };
4872
- } catch (err) {
4873
- return { ok: false, message: err instanceof Error ? err.message : String(err) };
4874
- } finally {
4875
- socket.disconnect();
5230
+ const report = result.data.committed ? {
5231
+ type: "task:work_saved",
5232
+ payload: {
5233
+ taskId: params.taskId,
5234
+ branchName: params.branchName,
5235
+ ...result.data.sha ? { sha: result.data.sha } : {}
5236
+ }
5237
+ } : null;
5238
+ return {
5239
+ attempted: true,
5240
+ committed: result.data.committed,
5241
+ pushedTo: result.data.pushedTo,
5242
+ problem: null,
5243
+ report
5244
+ };
5245
+ } catch (error) {
5246
+ return {
5247
+ ...NOTHING,
5248
+ attempted: true,
5249
+ problem: error instanceof Error ? error.message : String(error)
5250
+ };
4876
5251
  }
4877
5252
  }
5253
+ function describeRescue(result) {
5254
+ if (!result.attempted) return null;
5255
+ if (result.problem) return `Quiel: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043D\u0435\u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D\u043D\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u2014 ${result.problem}`;
5256
+ if (!result.committed) return null;
5257
+ return `Quiel: \u043D\u0435\u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430${result.pushedTo ? ` \u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430 \u0432 ${result.pushedTo}` : ""}`;
5258
+ }
4878
5259
 
4879
- // src/mcp/helpers.ts
4880
- function textResult(text) {
4881
- return { content: [{ type: "text", text }] };
5260
+ // src/socket/auth-reject.ts
5261
+ var AUTH_REJECT_REASONS = /* @__PURE__ */ new Set(["missing", "invalid"]);
5262
+ function isAuthRejection(error) {
5263
+ const message = error instanceof Error ? error.message : String(error);
5264
+ return AUTH_REJECT_REASONS.has(message.trim());
4882
5265
  }
4883
- function errorResult(text) {
4884
- return { content: [{ type: "text", text }], isError: true };
4885
- }
4886
- function leaseLostResult(taskLabel2, reason) {
4887
- return errorResult(
4888
- `\u0410\u0440\u0435\u043D\u0434\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 ${taskLabel2} \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430: ${reason}. \u041F\u0440\u0435\u043A\u0440\u0430\u0442\u0438 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430\u0434 \u043D\u0435\u0439 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E \u2014 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0441\u0434\u0430\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430, \u0432\u044B\u0437\u043E\u0432\u0438 next_task \u0437\u0430 \u043D\u043E\u0432\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435\u0439.`
4889
- );
5266
+ function describeAuthRejection(agentId) {
5267
+ return `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B\u0430 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 ${agentId}: \u043E\u043D \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D, \u043E\u0442\u043E\u0437\u0432\u0430\u043D \u0438\u043B\u0438 \u0438\u0441\u043F\u043E\u0440\u0447\u0435\u043D. \u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043D\u0435 \u043F\u043E\u043C\u043E\u0436\u0435\u0442 \u2014 \u0432\u044B\u043F\u0443\u0441\u0442\u0438 \u043D\u043E\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0430\u0433\u0435\u043D\u0442\u0430 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438 "quiel init" \u0437\u0430\u043D\u043E\u0432\u043E.`;
4890
5268
  }
4891
- function describeZodIssues(error) {
4892
- return error.issues.map(
4893
- (issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message
4894
- ).join("; ");
5269
+
5270
+ // src/socket/backoff.ts
5271
+ function nextReconnectDelayMs(attempt, options = {}) {
5272
+ const baseMs = options.baseMs ?? 1e3;
5273
+ const maxMs = options.maxMs ?? 3e4;
5274
+ const safeAttempt = Math.max(0, Math.trunc(attempt));
5275
+ const exponent = Math.min(safeAttempt, 30);
5276
+ const raw = baseMs * 2 ** exponent;
5277
+ return Math.min(raw, maxMs);
4895
5278
  }
4896
- function parseArgsOrError(schema, raw) {
4897
- const parsed = schema.safeParse(raw);
4898
- if (!parsed.success) {
4899
- return { ok: false, message: `\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B: ${describeZodIssues(parsed.error)}` };
4900
- }
4901
- return { ok: true, data: parsed.data };
5279
+
5280
+ // src/socket/client.ts
5281
+ import { EventEmitter } from "events";
5282
+
5283
+ // src/socket/emit-with-timeout.ts
5284
+ function toError(raw, context) {
5285
+ return raw instanceof Error ? raw : new Error(`${context}: ${String(raw)}`);
4902
5286
  }
4903
- async function callServerOrError(client, event, payload, schema, timeoutMs) {
4904
- let raw;
4905
- try {
4906
- raw = await client.callServer(event, payload, timeoutMs);
4907
- } catch (err) {
4908
- return {
4909
- ok: false,
4910
- message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B \u043D\u0430 ${event} (${err instanceof Error ? err.message : String(err)}). \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F \u0432 \u0444\u043E\u043D\u0435 \u2014 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439 \u0435\u0449\u0451 \u0440\u0430\u0437 \u0447\u0443\u0442\u044C \u043F\u043E\u0437\u0436\u0435.`
4911
- };
4912
- }
4913
- if (isAckError(raw)) {
4914
- return {
4915
- ok: false,
4916
- message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u0430\u0437\u0430\u043B\u0430 \u043D\u0430 ${event}: ${raw.error.code}: ${raw.error.message}`
4917
- };
4918
- }
4919
- const parsed = schema.safeParse(raw);
4920
- if (!parsed.success) {
4921
- return {
4922
- ok: false,
4923
- message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043F\u0440\u0438\u0441\u043B\u0430\u043B\u0430 \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 \u043D\u0430 ${event}: ${parsed.error.message}`
4924
- };
4925
- }
4926
- return { ok: true, data: parsed.data };
5287
+ async function emitWithAckTimeout(socket, event, payload, timeoutMs) {
5288
+ return await new Promise((resolve4, reject) => {
5289
+ const timer = setTimeout(() => {
5290
+ reject(new Error(`${event}: \u043D\u0435\u0442 ack \u043E\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0437\u0430 ${timeoutMs} \u043C\u0441`));
5291
+ }, timeoutMs);
5292
+ if (typeof timer.unref === "function") timer.unref();
5293
+ socket.emitWithAck(event, payload).then((value) => {
5294
+ clearTimeout(timer);
5295
+ resolve4(value);
5296
+ }).catch((err) => {
5297
+ clearTimeout(timer);
5298
+ reject(toError(err, event));
5299
+ });
5300
+ });
4927
5301
  }
4928
5302
 
4929
- // src/mcp/ask.ts
4930
- var askHumanInputShape = {
4931
- textMd: questionAskPayloadSchema.shape.textMd.describe(
4932
- "\u0422\u0435\u043A\u0441\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, Markdown. \u041F\u0438\u0448\u0438 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0431\u0435\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430 \u0441 \u043C\u043E\u0434\u0435\u043B\u044C\u044E."
4933
- ),
4934
- targetRole: questionAskPayloadSchema.shape.targetRole.describe(
4935
- '\u0420\u043E\u043B\u044C \u0430\u0434\u0440\u0435\u0441\u0430\u0442\u0430 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, "owner") \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u0443\u0432\u0438\u0434\u044F\u0442 \u0432\u0441\u0435 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441 \u044D\u0442\u043E\u0439 \u0440\u043E\u043B\u044C\u044E, \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u043F\u0435\u0440\u0432\u044B\u0439. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E.'
4936
- ),
4937
- targetMemberId: questionAskPayloadSchema.shape.targetMemberId.describe(
4938
- "ID \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0430\u0434\u0440\u0435\u0441\u0430\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E."
4939
- ),
4940
- wantsFiles: questionAskPayloadSchema.shape.wantsFiles.describe(
4941
- "\u041F\u043E\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u043A \u043E\u0442\u0432\u0435\u0442\u0443 (\u0441\u043A\u0440\u0438\u043D\u0448\u043E\u0442, \u043B\u043E\u0433, \u0432\u044B\u0433\u0440\u0443\u0437\u043A\u0443). \u0424\u043E\u0440\u043C\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443 \u0441 \u0432\u044B\u0431\u043E\u0440\u043E\u043C \u0444\u0430\u0439\u043B\u0430. \u042D\u0442\u043E \u041F\u0420\u041E\u0421\u042C\u0411\u0410, \u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435: \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0432\u043F\u0440\u0430\u0432\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u0431\u0435\u0437 \u0444\u0430\u0439\u043B\u043E\u0432, \u0438 \u0442\u043E\u0433\u0434\u0430 `attachments` \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u0431\u0443\u0434\u0435\u0442 \u043F\u0443\u0441\u0442\u044B\u043C."
4942
- ),
4943
- options: questionAskPayloadSchema.shape.options.describe(
4944
- "\u0412\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u043E\u0442\u0432\u0435\u0442\u0430 (\u043F\u043E\u043A\u0430\u0436\u0443\u0442\u0441\u044F \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438 \u0432 UI/Telegram). \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0439 \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C."
4945
- ),
4946
- blocking: questionAskPayloadSchema.shape.blocking.describe(
4947
- 'true \u2014 \u0442\u0432\u043E\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 \u0441\u0442\u0430\u0442\u0443\u0441 WAITING_HUMAN, \u0438 \u044D\u0442\u043E\u0442 \u0432\u044B\u0437\u043E\u0432 \u0441\u0430\u043C \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442 \u0434\u043E 600 \u0441; \u0435\u0441\u043B\u0438 \u0437\u0430 \u044D\u0442\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435\u0442, \u0432\u044B\u0437\u043E\u0432 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0441 \u043F\u043E\u043C\u0435\u0442\u043A\u043E\u0439 "pending" \u2014 \u0442\u043E\u0433\u0434\u0430 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430, \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430. false \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u043F\u0440\u043E\u0441\u0442\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F, \u0431\u0435\u0437 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0438 \u0431\u0435\u0437 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 (\u0434\u043B\u044F \u043D\u0435 \u0441\u0440\u043E\u0447\u043D\u044B\u0445 \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0439).'
4948
- )
4949
- };
4950
- var BLOCKING_WAIT_SEC = 600;
4951
- var ASK_ACK_BUFFER_MS = 15e3;
4952
- async function askHumanHandler(args, runtime) {
4953
- const state = readState(runtime.stateFilePath);
4954
- const taskId = state.currentTask?.taskId;
4955
- const waitSec = args.blocking ? BLOCKING_WAIT_SEC : 0;
4956
- const parsedArgs = parseArgsOrError(questionAskPayloadSchema, {
4957
- ...taskId ? { taskId } : {},
4958
- textMd: args.textMd,
4959
- ...args.targetRole ? { targetRole: args.targetRole } : {},
4960
- ...args.targetMemberId ? { targetMemberId: args.targetMemberId } : {},
4961
- ...args.options && args.options.length > 0 ? { options: args.options } : {},
4962
- ...args.wantsFiles ? { wantsFiles: true } : {},
4963
- blocking: args.blocking,
4964
- waitSec
4965
- });
4966
- if (!parsedArgs.ok) return errorResult(parsedArgs.message);
4967
- if (taskId) {
4968
- const lease = runtime.client.isLeaseLost(taskId);
4969
- if (lease.lost) return leaseLostResult(state.currentTask?.taskKey ?? taskId, lease.reason);
4970
- }
4971
- const result = await callServerOrError(
4972
- runtime.client,
4973
- "question:ask",
4974
- parsedArgs.data,
4975
- questionAskAckSchema,
4976
- waitSec * 1e3 + ASK_ACK_BUFFER_MS
4977
- );
4978
- if (!result.ok) return errorResult(result.message);
4979
- const settled = describeSettledQuestion(result.data.question);
4980
- if (settled) return textResult(settled);
4981
- if (args.blocking) {
4982
- return textResult(
4983
- `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0437\u0430\u0434\u0430\u043D, \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043D\u0430 \u0432 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 (WAITING_HUMAN). \u041E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E ${BLOCKING_WAIT_SEC} \u0441 (pending) \u2014 \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}", \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0436\u0434\u0430\u0442\u044C. \u041D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443 \u043F\u043E \u0437\u0430\u0434\u0430\u0447\u0435, \u043F\u043E\u043A\u0430 \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u0448\u044C \u043E\u0442\u0432\u0435\u0442.`
4984
- );
4985
- }
4986
- return textResult(
4987
- `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D (\u043D\u0435 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443). \u041E\u0442\u0432\u0435\u0442\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u2014 \u0435\u0441\u043B\u0438 \u043E\u043D \u043D\u0443\u0436\u0435\u043D, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C, \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}".`
4988
- );
5303
+ // src/socket/trust-cache.ts
5304
+ var UNKNOWN_TRUST_PROFILE_KEY = "unknown";
5305
+ function toTrustCache(rules, now = () => /* @__PURE__ */ new Date()) {
5306
+ return {
5307
+ profileKey: UNKNOWN_TRUST_PROFILE_KEY,
5308
+ updatedAt: now().toISOString(),
5309
+ rules
5310
+ };
4989
5311
  }
4990
- var waitAnswerInputShape = {
4991
- questionId: questionWaitPayloadSchema.shape.questionId.describe(
4992
- "ID \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u2014 \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430 ask_human (question.id) \u0438\u043B\u0438 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0433\u043E \u0432\u044B\u0437\u043E\u0432\u0430 wait_answer."
4993
- ),
4994
- waitSec: questionWaitPayloadSchema.shape.waitSec.optional().describe("\u0421\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0435\u043A\u0443\u043D\u0434 \u0436\u0434\u0430\u0442\u044C \u043E\u0442\u0432\u0435\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u044B\u0437\u043E\u0432\u0435, 0..600. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E 300.")
4995
- };
4996
- var DEFAULT_WAIT_ANSWER_SEC = 300;
4997
- var WAIT_ANSWER_ACK_BUFFER_MS = 15e3;
4998
- async function waitAnswerHandler(args, runtime) {
4999
- const waitSec = args.waitSec ?? DEFAULT_WAIT_ANSWER_SEC;
5000
- const parsedArgs = parseArgsOrError(questionWaitPayloadSchema, {
5001
- questionId: args.questionId,
5002
- waitSec
5003
- });
5004
- if (!parsedArgs.ok) return errorResult(parsedArgs.message);
5005
- const result = await callServerOrError(
5006
- runtime.client,
5007
- "question:wait",
5008
- parsedArgs.data,
5009
- questionWaitAckSchema,
5010
- waitSec * 1e3 + WAIT_ANSWER_ACK_BUFFER_MS
5011
- );
5012
- if (!result.ok) return errorResult(result.message);
5013
- const settled = describeSettledQuestion(result.data.question);
5014
- if (settled) return textResult(settled);
5015
- return textResult(
5016
- `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0432\u0441\u0451 \u0435\u0449\u0451 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 (\u0436\u0434\u0430\u043B ${waitSec} \u0441). \u0412\u044B\u0437\u043E\u0432\u0438 wait_answer \u0435\u0449\u0451 \u0440\u0430\u0437 \u0441 \u0442\u0435\u043C \u0436\u0435 questionId, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0443\u043C\u0435\u0441\u0442\u043D\u043E.`
5017
- );
5312
+
5313
+ // src/socket/types.ts
5314
+ function isAckError(raw) {
5315
+ return typeof raw === "object" && raw !== null && "error" in raw && typeof raw.error === "object";
5018
5316
  }
5019
- function describeAnswerAttachments(question) {
5020
- if (question.attachments.length === 0) return "";
5021
- const lines = question.attachments.map(
5022
- (file) => file.downloadUrl ? `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u043A\u0430\u0447\u0430\u0439: ${file.downloadUrl}` : `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430, \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442`
5023
- );
5024
- return `
5025
5317
 
5026
- \u041A \u043E\u0442\u0432\u0435\u0442\u0443 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u044B \u0444\u0430\u0439\u043B\u044B (${question.attachments.length}). \u042D\u0442\u043E \u0447\u0430\u0441\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u0430: \u0441\u043A\u0430\u0447\u0430\u0439 \u0438 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u041A\u0410\u0416\u0414\u042B\u0419, \u0441\u0441\u044B\u043B\u043A\u0438 \u0436\u0438\u0432\u0443\u0442 ${question.attachments[0]?.expiresInSeconds ?? 300} \u0441 \u2014 \u043D\u0435 \u043E\u0442\u043A\u043B\u0430\u0434\u044B\u0432\u0430\u0439.
5027
- ` + lines.join("\n");
5318
+ // src/socket/client.ts
5319
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
5320
+ var DEFAULT_ACK_TIMEOUT_MS = 1e4;
5321
+ var DEFAULT_BACKOFF_BASE_MS = 1e3;
5322
+ var DEFAULT_BACKOFF_MAX_MS = 3e4;
5323
+ function toError2(raw, context) {
5324
+ if (raw instanceof Error) return raw;
5325
+ return new Error(`${context}: ${String(raw)}`);
5028
5326
  }
5029
- function describeSettledQuestion(question) {
5030
- if (question.status === QuestionStatus.ANSWERED) {
5031
- return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043F\u043E\u043B\u0443\u0447\u0438\u043B \u043E\u0442\u0432\u0435\u0442:
5032
- ${question.answerMd ?? ""}${describeAnswerAttachments(question)}`;
5327
+ function resolveOptions(options) {
5328
+ return {
5329
+ roleKeys: options.roleKeys,
5330
+ cwd: options.cwd,
5331
+ clientVersion: options.clientVersion,
5332
+ agentVersion: options.agentVersion ?? "unknown",
5333
+ agentKind: options.agentKind ?? DEFAULT_AGENT_KIND,
5334
+ os: options.os ?? process.platform,
5335
+ // По умолчанию — файл ИМЕННО этого агента (state/store.ts::getStateFilePath(agentId)),
5336
+ // а не общий `state.json`: не трогаем src/state/**, но обязаны использовать
5337
+ // его правильно (OQ-05 — состояние на агента, не на машину).
5338
+ stateFilePath: options.stateFilePath ?? getStateFilePath(options.agentId),
5339
+ heartbeatIntervalMs: options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
5340
+ ackTimeoutMs: options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS,
5341
+ backoffBaseMs: options.backoffBaseMs ?? DEFAULT_BACKOFF_BASE_MS,
5342
+ backoffMaxMs: options.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS,
5343
+ now: options.now ?? (() => /* @__PURE__ */ new Date())
5344
+ };
5345
+ }
5346
+ var AgentSocketClient = class extends EventEmitter {
5347
+ socket;
5348
+ opts;
5349
+ agentId;
5350
+ stopped = true;
5351
+ reconnectAttempt = 0;
5352
+ reconnectTimer;
5353
+ heartbeatTimer;
5354
+ lastHelloAck;
5355
+ paused = false;
5356
+ pauseReasonText;
5357
+ leaseLostTasks = /* @__PURE__ */ new Map();
5358
+ constructor(options) {
5359
+ super();
5360
+ this.opts = resolveOptions(options);
5361
+ this.agentId = options.agentId;
5362
+ const factory = options.createSocket ?? createSocket;
5363
+ this.socket = factory(`${trimTrailingSlash2(options.url)}/agent`, {
5364
+ auth: { token: options.token },
5365
+ autoConnect: false,
5366
+ reconnection: false
5367
+ });
5368
+ this.registerSocketListeners();
5033
5369
  }
5034
- if (question.status === QuestionStatus.DISMISSED) {
5035
- return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043E\u0442\u043A\u043B\u043E\u043D\u0451\u043D (DISMISSED) \u2014 \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442. \u0420\u0435\u0448\u0438, \u043A\u0430\u043A \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043B\u044C\u0448\u0435, \u0441\u0430\u043C, \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0437\u0430\u043D\u043E\u0432\u043E.`;
5370
+ // -- Публичное API -----------------------------------------------------
5371
+ on(event, listener) {
5372
+ return super.on(event, listener);
5036
5373
  }
5037
- return void 0;
5038
- }
5039
-
5040
- // package.json
5041
- var package_default = {
5042
- name: "@quiel/cli",
5043
- version: "0.5.2",
5044
- description: "CLI \u0438 MCP-\u0441\u0435\u0440\u0432\u0435\u0440 Quiel: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0430\u0433\u0435\u043D\u0442\u0430 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 (Claude Code, Codex, Cursor, OpenCode) \u043A \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0435",
5045
- homepage: "https://quiel.app",
5046
- repository: {
5047
- type: "git",
5048
- url: "git+https://github.com/Quiel-App/quiel-cli.git"
5049
- },
5050
- bugs: {
5051
- url: "https://github.com/Quiel-App/quiel-cli/issues"
5052
- },
5053
- keywords: [
5054
- "quiel",
5055
- "claude-code",
5056
- "codex",
5057
- "cursor",
5058
- "opencode",
5059
- "mcp",
5060
- "mcp-server",
5061
- "ai-agents",
5062
- "task-tracker"
5063
- ],
5064
- license: "Apache-2.0",
5065
- type: "module",
5066
- bin: {
5067
- quiel: "dist/cli.js"
5068
- },
5069
- files: [
5070
- "dist",
5071
- "README.md",
5072
- "LICENSE"
5073
- ],
5074
- engines: {
5075
- node: ">=22"
5076
- },
5077
- publishConfig: {
5078
- access: "public"
5079
- },
5080
- scripts: {
5081
- build: "tsup",
5082
- dev: "tsup --watch",
5083
- lint: "biome check .",
5084
- typecheck: "tsc --noEmit",
5085
- test: "vitest run --passWithNoTests"
5086
- },
5087
- dependencies: {
5088
- "@modelcontextprotocol/sdk": "1.30.0",
5089
- commander: "15.0.0",
5090
- "socket.io-client": "4.8.3",
5091
- zod: "4.5.4"
5092
- },
5093
- devDependencies: {
5094
- "@quiel/config": "workspace:*",
5095
- "@quiel/shared": "workspace:*",
5096
- "@types/node": "20.19.43",
5097
- tsup: "8.5.1",
5098
- typescript: "7.0.2",
5099
- vitest: "5.0.0"
5374
+ once(event, listener) {
5375
+ return super.once(event, listener);
5100
5376
  }
5101
- };
5102
-
5103
- // src/version.ts
5104
- var VERSION = package_default.version;
5105
-
5106
- // src/mcp/constants.ts
5107
- var MCP_SERVER_NAME = "quiel";
5108
- var MCP_SERVER_VERSION = VERSION;
5109
-
5110
- // src/mcp/get-context.ts
5111
- var ALL_CONTEXT_INCLUDES = [
5112
- "spec",
5113
- "decisions",
5114
- // ВСЕ документы проекта — по умолчанию, а не по особому запросу.
5115
- // `spec`/`decisions` покрывают два типа из пяти, и человек, положивший
5116
- // контекст под типом `DATA` или `PLAN`, вправе ожидать, что агент его
5117
- // увидит. 14 сентября 2026 ровно этого и не случилось.
5118
- "documents",
5119
- "related",
5120
- "repo"
5121
- ];
5122
- var getContextInputShape = {
5123
- taskId: contextGetPayloadSchema.shape.taskId.describe(
5124
- "ID \u0437\u0430\u0434\u0430\u0447\u0438, \u0434\u043B\u044F \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u043D\u0443\u0436\u0435\u043D \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442. \u0415\u0441\u043B\u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0430\u0433\u0435\u043D\u0442\u0430 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C)."
5125
- ),
5126
- include: contextGetPayloadSchema.shape.include.optional().describe(
5127
- "\u0427\u0442\u043E \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C: spec | decisions | related | repo. \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u0438\u043B\u0438 \u043F\u0443\u0441\u0442 \u2014 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0432\u0441\u0451 \u0441\u0440\u0430\u0437\u0443."
5128
- )
5129
- };
5130
- async function getContextHandler(args, runtime) {
5131
- const state = readState(runtime.stateFilePath);
5132
- const taskId = args.taskId ?? state.currentTask?.taskId;
5133
- if (taskId) {
5134
- const lease = runtime.client.isLeaseLost(taskId);
5135
- if (lease.lost) return leaseLostResult(taskId, lease.reason);
5377
+ off(event, listener) {
5378
+ return super.off(event, listener);
5136
5379
  }
5137
- const include = args.include && args.include.length > 0 ? args.include : [...ALL_CONTEXT_INCLUDES];
5138
- const payload = contextGetPayloadSchema.parse({ ...taskId ? { taskId } : {}, include });
5139
- const result = await callServerOrError(
5140
- runtime.client,
5141
- "context:get",
5142
- payload,
5143
- contextPayloadSchema
5144
- );
5145
- if (!result.ok) return errorResult(result.message);
5146
- return textResult(JSON.stringify(result.data, null, 2));
5147
- }
5148
-
5149
- // src/mcp/get-task.ts
5150
- var getTaskInputShape = {
5151
- taskId: taskGetPayloadSchema.shape.taskId.describe("ID \u0437\u0430\u0434\u0430\u0447\u0438 (Task.id) \u2014 \u043D\u0435 \u043A\u043B\u044E\u0447 \u0432\u0438\u0434\u0430 REL-42")
5152
- };
5153
- async function getTaskHandler(args, runtime) {
5154
- const lease = runtime.client.isLeaseLost(args.taskId);
5155
- if (lease.lost) return leaseLostResult(args.taskId, lease.reason);
5156
- const payload = taskGetPayloadSchema.parse({ taskId: args.taskId });
5157
- const result = await callServerOrError(runtime.client, "task:get", payload, taskGetAckSchema);
5158
- if (!result.ok) return errorResult(result.message);
5159
- return textResult(JSON.stringify(result.data, null, 2));
5160
- }
5161
-
5162
- // src/mcp/log.ts
5163
- var logInputShape = {
5164
- message: taskProgressPayloadSchema.shape.note.describe("\u0421\u0442\u0440\u043E\u043A\u0430 \u0432 \u043B\u0435\u043D\u0442\u0443 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438"),
5165
- level: taskProgressPayloadSchema.shape.level.optional().describe("debug | info | warn | error, \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E info")
5166
- };
5167
- async function logHandler(args, runtime) {
5168
- const state = readState(runtime.stateFilePath);
5169
- if (!state.currentTask) {
5170
- return errorResult(
5171
- "\u041D\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0437\u0430\u043F\u0438\u0441\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430 \u0432 \u0435\u0451 \u043B\u0435\u043D\u0442\u0443. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0437\u043E\u0432\u0438 next_task."
5172
- );
5380
+ emit(event, ...args) {
5381
+ return super.emit(event, ...args);
5173
5382
  }
5174
- const lease = runtime.client.isLeaseLost(state.currentTask.taskId);
5175
- if (lease.lost) {
5176
- return leaseLostResult(state.currentTask.taskKey ?? state.currentTask.taskId, lease.reason);
5383
+ /** Поднимает соединение и heartbeat-таймер. Идемпотентно — повторный вызов на уже запущенном клиенте не делает ничего. */
5384
+ start() {
5385
+ if (!this.stopped) return;
5386
+ this.stopped = false;
5387
+ this.reconnectAttempt = 0;
5388
+ this.startHeartbeatTimer();
5389
+ this.socket.connect();
5390
+ }
5391
+ /** Останавливает heartbeat, отменяет запланированное переподключение, закрывает сокет. После `stop()` клиент больше не переподключается сам. */
5392
+ stop() {
5393
+ this.stopped = true;
5394
+ if (this.reconnectTimer) {
5395
+ clearTimeout(this.reconnectTimer);
5396
+ this.reconnectTimer = void 0;
5397
+ }
5398
+ this.stopHeartbeatTimer();
5399
+ this.socket.disconnect();
5400
+ }
5401
+ get connected() {
5402
+ return this.socket.connected;
5403
+ }
5404
+ get lastHello() {
5405
+ return this.lastHelloAck;
5406
+ }
5407
+ get isPaused() {
5408
+ return this.paused;
5409
+ }
5410
+ get pauseReason() {
5411
+ return this.pauseReasonText;
5412
+ }
5413
+ /** §11.2 ТЗ: «следующий вызов любого тула по этой задаче должен сообщить модели, что работу надо прекратить» — тулы (P2-12) сверяются с этим геттером. */
5414
+ isLeaseLost(taskId) {
5415
+ const reason = this.leaseLostTasks.get(taskId);
5416
+ return reason === void 0 ? { lost: false } : { lost: true, reason };
5417
+ }
5418
+ /**
5419
+ * Общий emit+ack поверх ЭТОГО ЖЕ постоянного сокета — тем же приёмом
5420
+ * (`emitWithAckTimeout`), которым `sendHello`/`sendHeartbeat` уже
5421
+ * пользуются внутри класса, но публично: MCP-тулы (P2-12/13,
5422
+ * `../mcp/**`) шлют через него `task:claim`/`task:start`/`task:progress`/
5423
+ * `task:get`/`task:report`/`task:release`/`context:get` (§11.1 ТЗ) — то
5424
+ * есть ВСЁ, что не `agent:hello`/`agent:heartbeat`/`agent:mode` (у
5425
+ * первых двух своя логика согласования состояния в этом классе, третий
5426
+ * — короткоживущий отдельный сокет, см. `send-agent-mode.ts`). Нарочно
5427
+ * НЕ отдельное соединение: `task:claim` — блокирующий долгий long-poll,
5428
+ * и держать его на том же сокете, где уже идёт heartbeat/presence этого
5429
+ * агента, а не поднимать вторую параллельную сессию — единственный
5430
+ * вариант, не удваивающий presence на сервере.
5431
+ */
5432
+ callServer(event, payload, timeoutMs = this.opts.ackTimeoutMs) {
5433
+ return emitWithAckTimeout(this.socket, event, payload, timeoutMs);
5434
+ }
5435
+ /**
5436
+ * Ждёт первого успешного `agent:hello` не дольше `timeoutMs` (по
5437
+ * умолчанию 5 с — как раньше HTTP-пинг в `network.ts`). Таймаут НЕ
5438
+ * останавливает клиент: переподключение по backoff продолжается в фоне,
5439
+ * это только ограничение по времени для вызывающей стороны (`connect.ts`).
5440
+ */
5441
+ waitForFirstConnection(timeoutMs = 5e3) {
5442
+ return new Promise((resolve4) => {
5443
+ let settled = false;
5444
+ const onConnected = (ack) => {
5445
+ if (settled) return;
5446
+ settled = true;
5447
+ clearTimeout(timer);
5448
+ this.off("connected", onConnected);
5449
+ resolve4({ ok: true, ack });
5450
+ };
5451
+ const onAuthRejected = (message) => {
5452
+ if (settled) return;
5453
+ settled = true;
5454
+ clearTimeout(timer);
5455
+ this.off("connected", onConnected);
5456
+ this.off("auth-rejected", onAuthRejected);
5457
+ resolve4({ ok: false, message });
5458
+ };
5459
+ const timer = setTimeout(() => {
5460
+ if (settled) return;
5461
+ settled = true;
5462
+ this.off("connected", onConnected);
5463
+ this.off("auth-rejected", onAuthRejected);
5464
+ resolve4({
5465
+ ok: false,
5466
+ message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 agent:hello \u0437\u0430 ${timeoutMs} \u043C\u0441 \u2014 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430. \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0432 \u0444\u043E\u043D\u0435 (backoff \xA711.4 \u0422\u0417).`
5467
+ });
5468
+ }, timeoutMs);
5469
+ if (typeof timer.unref === "function") timer.unref();
5470
+ this.on("connected", onConnected);
5471
+ this.on("auth-rejected", onAuthRejected);
5472
+ });
5473
+ }
5474
+ // -- Подключение --------------------------------------------------------
5475
+ registerSocketListeners() {
5476
+ this.socket.on("connect", () => this.handleConnect());
5477
+ this.socket.on("connect_error", (err) => this.handleConnectError(err));
5478
+ this.socket.on("disconnect", (reason) => this.handleDisconnect(String(reason)));
5479
+ this.socket.on("lease:lost", (raw) => this.handleLeaseLost(raw));
5480
+ this.socket.on("mode:set", (raw) => this.handleModeSet(raw));
5481
+ this.socket.on("agent:pause", (raw) => this.handleAgentPause(raw));
5482
+ this.socket.on("trust:updated", (raw) => this.handleTrustUpdated(raw));
5483
+ this.socket.on("task:updated", (raw) => this.handleTaskUpdated(raw));
5484
+ this.socket.on("question:answered", (raw) => this.handleQuestionAnswered(raw));
5485
+ this.socket.on("approval:decided", (raw) => this.handleApprovalDecided(raw));
5486
+ }
5487
+ handleConnect() {
5488
+ if (this.stopped) return;
5489
+ this.reconnectAttempt = 0;
5490
+ void this.sendHello();
5491
+ }
5492
+ handleConnectError(err) {
5493
+ if (isAuthRejection(err)) {
5494
+ this.stop();
5495
+ this.emit("auth-rejected", describeAuthRejection(this.agentId));
5496
+ return;
5497
+ }
5498
+ this.emit("client-error", toError2(err, "connect_error"));
5499
+ this.scheduleReconnect();
5500
+ }
5501
+ handleDisconnect(reason) {
5502
+ this.emit("disconnected", reason);
5503
+ if (this.stopped) return;
5504
+ this.scheduleReconnect();
5505
+ }
5506
+ scheduleReconnect() {
5507
+ if (this.stopped || this.reconnectTimer) return;
5508
+ const delayMs = nextReconnectDelayMs(this.reconnectAttempt, {
5509
+ baseMs: this.opts.backoffBaseMs,
5510
+ maxMs: this.opts.backoffMaxMs
5511
+ });
5512
+ this.reconnectAttempt += 1;
5513
+ this.emit("reconnecting", { attempt: this.reconnectAttempt, delayMs });
5514
+ this.reconnectTimer = setTimeout(() => {
5515
+ this.reconnectTimer = void 0;
5516
+ if (this.stopped) return;
5517
+ this.socket.connect();
5518
+ }, delayMs);
5519
+ }
5520
+ // -- agent:hello ---------------------------------------------------------
5521
+ async sendHello() {
5522
+ const state = readState(this.opts.stateFilePath);
5523
+ const payload = agentHelloPayloadSchema.parse({
5524
+ clientVersion: this.opts.clientVersion,
5525
+ cwd: this.opts.cwd,
5526
+ os: this.opts.os,
5527
+ agentKind: this.opts.agentKind,
5528
+ agentVersion: this.opts.agentVersion,
5529
+ // `claudeVersion` шлётся ТОЛЬКО для Claude Code — и только чтобы
5530
+ // клиент работал против ещё не обновлённого сервера, который знает
5531
+ // одно это поле. Для Codex поле с таким именем было бы неправдой.
5532
+ ...this.opts.agentKind === AgentKind.CLAUDE_CODE ? { claudeVersion: this.opts.agentVersion } : {},
5533
+ roleKeys: [...this.opts.roleKeys],
5534
+ ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {}
5535
+ });
5536
+ let raw;
5537
+ try {
5538
+ raw = await emitWithAckTimeout(this.socket, "agent:hello", payload, this.opts.ackTimeoutMs);
5539
+ } catch (err) {
5540
+ this.emit("client-error", toError2(err, "agent:hello"));
5541
+ return;
5542
+ }
5543
+ if (this.stopped) return;
5544
+ if (isAckError(raw)) {
5545
+ this.emit("client-error", new Error(`agent:hello: ${raw.error.code}: ${raw.error.message}`));
5546
+ return;
5547
+ }
5548
+ const parsed = agentHelloAckSchema.safeParse(raw);
5549
+ if (!parsed.success) {
5550
+ this.emit(
5551
+ "client-error",
5552
+ new Error(`agent:hello: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
5553
+ );
5554
+ return;
5555
+ }
5556
+ const ack = parsed.data;
5557
+ this.lastHelloAck = ack;
5558
+ this.paused = false;
5559
+ this.pauseReasonText = void 0;
5560
+ this.applyHelloAck(ack);
5561
+ this.emit("connected", ack);
5562
+ }
5563
+ applyHelloAck(ack) {
5564
+ const state = readState(this.opts.stateFilePath);
5565
+ const patch = {
5566
+ // OQ-06: сервер — источник правды, локальный режим перекрывается безусловно.
5567
+ mode: ack.mode,
5568
+ trust: toTrustCache(ack.trust, this.opts.now)
5569
+ };
5570
+ if (!ack.currentTask || ack.currentTask.id !== state.currentTask?.taskId) {
5571
+ patch.currentTask = null;
5572
+ }
5573
+ updateState(patch, this.opts.stateFilePath);
5574
+ }
5575
+ // -- agent:heartbeat -------------------------------------------------------
5576
+ startHeartbeatTimer() {
5577
+ if (this.heartbeatTimer) return;
5578
+ this.heartbeatTimer = setInterval(() => {
5579
+ void this.sendHeartbeat();
5580
+ }, this.opts.heartbeatIntervalMs);
5581
+ }
5582
+ stopHeartbeatTimer() {
5583
+ if (!this.heartbeatTimer) return;
5584
+ clearInterval(this.heartbeatTimer);
5585
+ this.heartbeatTimer = void 0;
5586
+ }
5587
+ async sendHeartbeat() {
5588
+ if (!this.socket.connected) return;
5589
+ const state = readState(this.opts.stateFilePath);
5590
+ const payload = agentHeartbeatPayloadSchema.parse({
5591
+ ...state.currentTask?.taskId ? { currentTaskId: state.currentTask.taskId } : {},
5592
+ ...state.currentTask?.resumeToken ? { resumeToken: state.currentTask.resumeToken } : {},
5593
+ status: state.currentTask ? AgentStatus.BUSY : AgentStatus.IDLE
5594
+ });
5595
+ let raw;
5596
+ try {
5597
+ raw = await emitWithAckTimeout(
5598
+ this.socket,
5599
+ "agent:heartbeat",
5600
+ payload,
5601
+ this.opts.ackTimeoutMs
5602
+ );
5603
+ } catch (err) {
5604
+ this.emit("client-error", toError2(err, "agent:heartbeat"));
5605
+ return;
5606
+ }
5607
+ if (isAckError(raw)) {
5608
+ this.emit(
5609
+ "client-error",
5610
+ new Error(`agent:heartbeat: ${raw.error.code}: ${raw.error.message}`)
5611
+ );
5612
+ return;
5613
+ }
5614
+ const parsed = agentHeartbeatAckSchema.safeParse(raw);
5615
+ if (!parsed.success) {
5616
+ this.emit(
5617
+ "client-error",
5618
+ new Error(`agent:heartbeat: \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}`)
5619
+ );
5620
+ return;
5621
+ }
5622
+ this.emit("heartbeat", parsed.data);
5623
+ }
5624
+ // -- Пуш-события сервера (§11.2 ТЗ) --------------------------------------
5625
+ handleLeaseLost(raw) {
5626
+ const parsed = leaseLostPayloadSchema.safeParse(raw);
5627
+ if (!parsed.success) {
5628
+ this.emit(
5629
+ "client-error",
5630
+ new Error(`lease:lost: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5631
+ );
5632
+ return;
5633
+ }
5634
+ const { taskId, reason } = parsed.data;
5635
+ this.leaseLostTasks.set(taskId, reason);
5636
+ const state = readState(this.opts.stateFilePath);
5637
+ if (state.currentTask?.taskId === taskId) {
5638
+ updateState({ currentTask: null }, this.opts.stateFilePath);
5639
+ }
5640
+ this.emit("lease:lost", parsed.data);
5177
5641
  }
5178
- const payload = taskProgressPayloadSchema.parse({
5179
- taskId: state.currentTask.taskId,
5180
- note: args.message,
5181
- ...args.level ? { level: args.level } : {}
5182
- });
5183
- const result = await callServerOrError(
5184
- runtime.client,
5185
- "task:progress",
5186
- payload,
5187
- taskProgressAckSchema
5188
- );
5189
- if (!result.ok) return errorResult(result.message);
5190
- if (!result.data.ok) return errorResult("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u0437\u0430\u043F\u0438\u0441\u044C \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 (ok:false).");
5191
- return textResult("\u0417\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438.");
5192
- }
5193
-
5194
- // src/git/result.ts
5195
- function gitOk(data) {
5196
- return { ok: true, data };
5197
- }
5198
- function gitErr(message) {
5199
- return { ok: false, message };
5200
- }
5201
-
5202
- // src/git/branch-name.ts
5203
- var FORBIDDEN_CHARS = /[\s~^:?*[\\\x00-\x1f\x7f]/;
5204
- function isValidGitBranchName(name) {
5205
- if (name.length === 0 || name.length > 255) return false;
5206
- if (name.startsWith("-")) return false;
5207
- if (name.startsWith("/") || name.endsWith("/")) return false;
5208
- if (name.endsWith(".")) return false;
5209
- if (name.includes("..")) return false;
5210
- if (name.includes("//")) return false;
5211
- if (name.includes("@{")) return false;
5212
- if (name === "@") return false;
5213
- if (FORBIDDEN_CHARS.test(name)) return false;
5214
- const segments = name.split("/");
5215
- for (const segment of segments) {
5216
- if (segment.length === 0) return false;
5217
- if (segment.startsWith(".")) return false;
5218
- if (segment.endsWith(".lock")) return false;
5642
+ handleModeSet(raw) {
5643
+ const parsed = modeSetPayloadSchema.safeParse(raw);
5644
+ if (!parsed.success) {
5645
+ this.emit("client-error", new Error(`mode:set: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`));
5646
+ return;
5647
+ }
5648
+ updateState({ mode: parsed.data.mode }, this.opts.stateFilePath);
5649
+ this.emit("mode:set", parsed.data);
5219
5650
  }
5220
- return true;
5221
- }
5222
- function requireValidBranchName(name, label) {
5223
- if (!isValidGitBranchName(name)) {
5224
- return gitErr(
5225
- `${label} "${name}" \u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0438\u043C\u044F git-\u0432\u0435\u0442\u043A\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
5226
- );
5651
+ handleAgentPause(raw) {
5652
+ const parsed = agentPausePayloadSchema.safeParse(raw);
5653
+ if (!parsed.success) {
5654
+ this.emit(
5655
+ "client-error",
5656
+ new Error(`agent:pause: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5657
+ );
5658
+ return;
5659
+ }
5660
+ this.paused = true;
5661
+ this.pauseReasonText = parsed.data.reason;
5662
+ this.emit("pause", parsed.data);
5227
5663
  }
5228
- return gitOk(name);
5229
- }
5230
-
5231
- // src/git/exec.ts
5232
- import { execFile } from "child_process";
5233
- var DEFAULT_TIMEOUT_MS2 = 12e4;
5234
- var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
5235
- function runGit(args, options) {
5236
- return new Promise((resolvePromise) => {
5237
- execFile(
5238
- "git",
5239
- [...args],
5240
- {
5241
- cwd: options.cwd,
5242
- shell: false,
5243
- timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5244
- maxBuffer: MAX_BUFFER_BYTES,
5245
- env: {
5246
- ...process.env,
5247
- ...options.env,
5248
- // Без человека за терминалом: если git вдруг решит спросить
5249
- // логин/пароль, зависший prompt выглядел бы как неожиданное
5250
- // поведение (требование подзадачи: «любая неожиданность —
5251
- // понятный отказ, а не попробую починить»), а не как честный
5252
- // отказ команды с кодом возврата.
5253
- GIT_TERMINAL_PROMPT: "0"
5254
- }
5255
- },
5256
- (error, stdout, stderr) => {
5257
- if (!error) {
5258
- resolvePromise({ ok: true, code: 0, signal: null, stdout, stderr });
5259
- return;
5260
- }
5261
- const code = typeof error.code === "number" ? error.code : null;
5262
- resolvePromise({
5263
- ok: false,
5264
- code,
5265
- signal: error.signal ?? null,
5266
- stdout,
5267
- // Если git ничего не написал в stderr (например, сам процесс не
5268
- // запустился — ENOENT), берём текст ошибки Node, чтобы вызывающий
5269
- // код не остался с пустым сообщением.
5270
- stderr: stderr.length > 0 ? stderr : error.message
5271
- });
5272
- }
5273
- );
5274
- });
5275
- }
5276
- function describeGitFailure(commandLabel, result) {
5277
- const stderr = result.stderr.trim();
5278
- const codeText = result.code === null ? "" : ` (\u043A\u043E\u0434 ${result.code})`;
5279
- return stderr.length > 0 ? `${commandLabel}${codeText}: ${stderr}` : `${commandLabel} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439${codeText}`;
5280
- }
5281
-
5282
- // src/git/repo.ts
5283
- async function localBranchExists(repoRoot, branch) {
5284
- const result = await runGit(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
5285
- cwd: repoRoot
5286
- });
5287
- return result.ok;
5288
- }
5289
- async function getHeadSha(worktreeDir) {
5290
- const result = await runGit(["rev-parse", "HEAD"], { cwd: worktreeDir });
5291
- if (!result.ok) {
5292
- return gitErr(describeGitFailure("git rev-parse HEAD", result));
5664
+ handleTrustUpdated(raw) {
5665
+ const parsed = trustUpdatedPayloadSchema.safeParse(raw);
5666
+ if (!parsed.success) {
5667
+ this.emit(
5668
+ "client-error",
5669
+ new Error(`trust:updated: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5670
+ );
5671
+ return;
5672
+ }
5673
+ updateState({ trust: toTrustCache(parsed.data, this.opts.now) }, this.opts.stateFilePath);
5674
+ this.emit("trust:updated", parsed.data);
5293
5675
  }
5294
- return gitOk(result.stdout.trim());
5295
- }
5296
- async function currentBranch(worktreeDir) {
5297
- const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreeDir });
5298
- if (!result.ok) {
5299
- return gitErr(describeGitFailure("git rev-parse --abbrev-ref HEAD", result));
5676
+ handleTaskUpdated(raw) {
5677
+ const parsed = taskUpdatedAgentPayloadSchema.safeParse(raw);
5678
+ if (parsed.success) this.emit("task:updated", parsed.data);
5300
5679
  }
5301
- return gitOk(result.stdout.trim());
5302
- }
5303
-
5304
- // src/git/status.ts
5305
- var CONFLICT_CODES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
5306
- async function getWorktreeStatus(worktreeDir) {
5307
- const result = await runGit(["status", "--porcelain=v1", "--untracked-files=all"], {
5308
- cwd: worktreeDir
5309
- });
5310
- if (!result.ok) {
5311
- return gitErr(describeGitFailure("git status", result));
5680
+ handleQuestionAnswered(raw) {
5681
+ const parsed = questionAnsweredPayloadSchema.safeParse(raw);
5682
+ if (parsed.success) this.emit("question:answered", parsed.data);
5312
5683
  }
5313
- const changedPaths = [];
5314
- const conflictedPaths = [];
5315
- for (const line of result.stdout.split("\n")) {
5316
- if (line.length === 0) continue;
5317
- const code = line.slice(0, 2);
5318
- const rawPath = line.slice(3);
5319
- const arrowIndex = rawPath.indexOf(" -> ");
5320
- const path = arrowIndex >= 0 ? rawPath.slice(arrowIndex + 4) : rawPath;
5321
- changedPaths.push(path);
5322
- if (CONFLICT_CODES.has(code)) conflictedPaths.push(path);
5684
+ /**
5685
+ * §14.4 ТЗ, п.4-5 — асинхронный путь (в дополнение к синхронному в
5686
+ * `mcp/request-approval.ts::settleApproval`, который срабатывает, когда
5687
+ * решение приходит в рамках ОДНОГО вызова `request_approval`/
5688
+ * `wait_approval`). Этот пуш ловит случай, когда решение пришло уже
5689
+ * ПОСЛЕ того, как собственный long-poll тула истёк (вернул `PENDING`),
5690
+ * а модель ещё не успела/не стала звать `wait_approval` снова — на
5691
+ * постоянном соединении (`quiel mcp`, единственный держатель ЭТОГО
5692
+ * класса) `approval:decided` долетит и без явного повторного вызова.
5693
+ *
5694
+ * `approvalDecidedPayloadSchema` несёт только `approvalId`+`status`+
5695
+ * `oneTimeToken?` — НЕ `command`/`cwd` (провод их не отдаёт, см.
5696
+ * `state/schema.ts::PendingApprovalSchema`), поэтому command+cwd берутся
5697
+ * из `state.pendingApprovals[approvalId]`, куда их кладёт
5698
+ * `request_approval` сразу после создания запроса. Если записи там нет
5699
+ * (approval создан не этим клиентом/до перезапуска процесса, либо уже
5700
+ * обработан) — писать пока нечего, `pendingApprovals` и так пуст.
5701
+ */
5702
+ handleApprovalDecided(raw) {
5703
+ const parsed = approvalDecidedPayloadSchema.safeParse(raw);
5704
+ if (!parsed.success) {
5705
+ this.emit(
5706
+ "client-error",
5707
+ new Error(`approval:decided: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 payload: ${parsed.error.message}`)
5708
+ );
5709
+ return;
5710
+ }
5711
+ this.applyApprovalDecided(parsed.data);
5712
+ this.emit("approval:decided", parsed.data);
5323
5713
  }
5324
- return gitOk({ clean: changedPaths.length === 0, changedPaths, conflictedPaths });
5714
+ applyApprovalDecided(payload) {
5715
+ const state = readState(this.opts.stateFilePath);
5716
+ const pending = state.pendingApprovals[payload.approvalId];
5717
+ if (!pending) return;
5718
+ if (payload.status === ApprovalStatus.APPROVED && payload.oneTimeToken) {
5719
+ const approvalTtlMin = state.trust?.rules.approvalTtlMin ?? 60;
5720
+ const expiresAt = new Date(this.opts.now().getTime() + approvalTtlMin * 6e4).toISOString();
5721
+ updateState(
5722
+ {
5723
+ ...storeApprovedCommand(
5724
+ state,
5725
+ { command: pending.command, cwd: pending.cwd, token: payload.oneTimeToken, expiresAt },
5726
+ this.opts.now
5727
+ ),
5728
+ ...clearPendingApproval(state, payload.approvalId)
5729
+ },
5730
+ this.opts.stateFilePath
5731
+ );
5732
+ return;
5733
+ }
5734
+ updateState(clearPendingApproval(state, payload.approvalId), this.opts.stateFilePath);
5735
+ }
5736
+ };
5737
+ function trimTrailingSlash2(url) {
5738
+ return url.endsWith("/") ? url.slice(0, -1) : url;
5325
5739
  }
5326
5740
 
5327
- // src/git/commit.ts
5328
- function buildCommitMessage(params) {
5329
- const subject = `${params.commitPrefix} ${params.title}`.trim();
5330
- return `${subject}
5331
-
5332
- ${buildTaskTrailer(params.taskKey)}
5333
- `;
5741
+ // src/socket/send-agent-mode.ts
5742
+ var DEFAULT_TIMEOUT_MS2 = 8e3;
5743
+ function trimTrailingSlash3(url) {
5744
+ return url.endsWith("/") ? url.slice(0, -1) : url;
5334
5745
  }
5335
- async function commitAll(params) {
5336
- const addResult = await runGit(["add", "-A"], { cwd: params.worktreeDir });
5337
- if (!addResult.ok) {
5338
- return gitErr(describeGitFailure("git add -A", addResult));
5339
- }
5340
- const status = await getWorktreeStatus(params.worktreeDir);
5341
- if (!status.ok) return status;
5342
- if (status.data.clean) {
5343
- return gitOk({ committed: false, sha: null });
5344
- }
5345
- const message = buildCommitMessage(params);
5346
- const commitResult = await runGit(["commit", "-m", message], { cwd: params.worktreeDir });
5347
- if (!commitResult.ok) {
5348
- return gitErr(describeGitFailure("git commit", commitResult));
5746
+ async function sendAgentMode(options) {
5747
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
5748
+ const factory = options.createSocket ?? createSocket;
5749
+ const socket = factory(`${trimTrailingSlash3(options.url)}/agent`, {
5750
+ auth: { token: options.token },
5751
+ autoConnect: false,
5752
+ reconnection: false
5753
+ });
5754
+ try {
5755
+ await connectOnce(socket, timeoutMs);
5756
+ const payload = agentModePayloadSchema.parse({ mode: options.mode });
5757
+ const raw = await emitWithAckTimeout(socket, "agent:mode", payload, timeoutMs);
5758
+ if (isAckError(raw)) {
5759
+ return { ok: false, message: `${raw.error.code}: ${raw.error.message}` };
5760
+ }
5761
+ const parsed = agentModeAckSchema.safeParse(raw);
5762
+ if (!parsed.success) {
5763
+ return { ok: false, message: `\u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 ack: ${parsed.error.message}` };
5764
+ }
5765
+ if (!parsed.data.ok) {
5766
+ return { ok: false, message: "\u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B agent:mode (ok:false)" };
5767
+ }
5768
+ return { ok: true, mode: parsed.data.mode ?? options.mode };
5769
+ } catch (err) {
5770
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
5771
+ } finally {
5772
+ socket.disconnect();
5349
5773
  }
5350
- const sha = await getHeadSha(params.worktreeDir);
5351
- return gitOk({ committed: true, sha: sha.ok ? sha.data : null });
5352
5774
  }
5353
5775
 
5354
- // src/git/worktree.ts
5355
- import { createHash as createHash2 } from "crypto";
5356
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, realpathSync } from "fs";
5357
- import { dirname as dirname4, isAbsolute, join as join9, relative, resolve as resolve2 } from "path";
5358
- var WORKTREES_RELATIVE_ROOT = join9(".claude", "worktrees");
5359
- function getDefaultWorktreesRoot(repoRoot) {
5360
- return join9(repoRoot, WORKTREES_RELATIVE_ROOT);
5776
+ // src/mcp/helpers.ts
5777
+ function textResult(text) {
5778
+ return { content: [{ type: "text", text }] };
5361
5779
  }
5362
- function resolveWorktreeDir(worktreesRoot, repoRoot, branchName) {
5363
- if (isInsideRepo(worktreesRoot, repoRoot)) return join9(worktreesRoot, branchName);
5364
- const repoHash = createHash2("sha1").update(resolve2(repoRoot)).digest("hex").slice(0, 12);
5365
- return join9(worktreesRoot, repoHash, branchName);
5780
+ function errorResult(text) {
5781
+ return { content: [{ type: "text", text }], isError: true };
5366
5782
  }
5367
- function isInsideRepo(candidate, repoRoot) {
5368
- const rel = relative(resolve2(repoRoot), resolve2(candidate));
5369
- return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
5783
+ function leaseLostResult(taskLabel2, reason) {
5784
+ return errorResult(
5785
+ `\u0410\u0440\u0435\u043D\u0434\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 ${taskLabel2} \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430: ${reason}. \u041F\u0440\u0435\u043A\u0440\u0430\u0442\u0438 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430\u0434 \u043D\u0435\u0439 \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E \u2014 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0441\u0434\u0430\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430, \u0432\u044B\u0437\u043E\u0432\u0438 next_task \u0437\u0430 \u043D\u043E\u0432\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435\u0439.`
5786
+ );
5370
5787
  }
5371
- function canonicalPath(path) {
5372
- try {
5373
- return realpathSync(path);
5374
- } catch {
5375
- return resolve2(path);
5376
- }
5788
+ function describeZodIssues(error) {
5789
+ return error.issues.map(
5790
+ (issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message
5791
+ ).join("; ");
5377
5792
  }
5378
- async function listWorktrees(repoRoot) {
5379
- const result = await runGit(["worktree", "list", "--porcelain"], { cwd: repoRoot });
5380
- if (!result.ok) {
5381
- return gitErr(describeGitFailure("git worktree list", result));
5382
- }
5383
- const entries = [];
5384
- let path;
5385
- let branch = null;
5386
- const flush = () => {
5387
- if (path) entries.push({ path, branch });
5388
- path = void 0;
5389
- branch = null;
5390
- };
5391
- for (const line of result.stdout.split("\n")) {
5392
- if (line.length === 0) {
5393
- flush();
5394
- continue;
5395
- }
5396
- if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
5397
- else if (line.startsWith("branch refs/heads/")) branch = line.slice("branch refs/heads/".length);
5793
+ function parseArgsOrError(schema, raw) {
5794
+ const parsed = schema.safeParse(raw);
5795
+ if (!parsed.success) {
5796
+ return { ok: false, message: `\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B: ${describeZodIssues(parsed.error)}` };
5398
5797
  }
5399
- flush();
5400
- return gitOk(entries);
5798
+ return { ok: true, data: parsed.data };
5401
5799
  }
5402
- async function ensureWorktree(params) {
5403
- const remote = params.remote ?? "origin";
5404
- const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
5405
- if (!branchCheck.ok) return branchCheck;
5406
- const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
5407
- if (!defaultBranchCheck.ok) return defaultBranchCheck;
5408
- const worktreesRoot = params.worktreesRoot ?? getDefaultWorktreesRoot(params.repoRoot);
5409
- const worktreeDir = resolveWorktreeDir(worktreesRoot, params.repoRoot, params.branchName);
5410
- const list = await listWorktrees(params.repoRoot);
5411
- if (!list.ok) return list;
5412
- const existing = list.data.find((wt) => canonicalPath(wt.path) === canonicalPath(worktreeDir));
5413
- if (existing) {
5414
- if (existing.branch === params.branchName) {
5415
- return gitOk({ worktreeDir, reused: true });
5416
- }
5417
- return gitErr(
5418
- `\u041F\u043E \u043F\u0443\u0442\u0438 ${worktreeDir} \u0443\u0436\u0435 \u0435\u0441\u0442\u044C git worktree \u043D\u0430 \u0432\u0435\u0442\u043A\u0435 "${existing.branch ?? "(\u043E\u0442\u0441\u043E\u0435\u0434\u0438\u043D\u0451\u043D\u043D\u044B\u0439 HEAD)"}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}". \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0440\u0443\u043A\u0430\u043C\u0438: "git -C ${params.repoRoot} worktree remove ${worktreeDir}" (\u0434\u043E\u0431\u0430\u0432\u044C "--force", \u0435\u0441\u043B\u0438 \u0442\u0430\u043C \u043D\u0435\u0442 \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u0430\u0436\u043D\u043E\u0433\u043E), \u0437\u0430\u0442\u0435\u043C \u043F\u043E\u0432\u0442\u043E\u0440\u0438.`
5419
- );
5420
- }
5421
- if (existsSync9(worktreeDir)) {
5422
- return gitErr(
5423
- `\u041F\u0443\u0442\u044C ${worktreeDir} \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E git \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0435\u0433\u043E \u0441\u0432\u043E\u0435\u0439 worktree. \u0423\u0431\u0435\u0440\u0438 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u2014 \u0442\u0440\u043E\u0433\u0430\u0442\u044C \u0447\u0443\u0436\u043E\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E.`
5424
- );
5425
- }
5426
- await runGit(["worktree", "prune"], { cwd: params.repoRoot });
5427
- const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
5428
- cwd: params.repoRoot
5429
- });
5430
- if (!fetchResult.ok) {
5431
- return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
5800
+ async function callServerOrError(client, event, payload, schema, timeoutMs) {
5801
+ let raw;
5802
+ try {
5803
+ raw = await client.callServer(event, payload, timeoutMs);
5804
+ } catch (err) {
5805
+ return {
5806
+ ok: false,
5807
+ message: `\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B \u043D\u0430 ${event} (${err instanceof Error ? err.message : String(err)}). \u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F \u0432 \u0444\u043E\u043D\u0435 \u2014 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439 \u0435\u0449\u0451 \u0440\u0430\u0437 \u0447\u0443\u0442\u044C \u043F\u043E\u0437\u0436\u0435.`
5808
+ };
5432
5809
  }
5433
- mkdirSync6(dirname4(worktreeDir), { recursive: true });
5434
- const branchExists = await localBranchExists(params.repoRoot, params.branchName);
5435
- const addArgs = branchExists ? ["worktree", "add", worktreeDir, params.branchName] : ["worktree", "add", "-b", params.branchName, worktreeDir, `${remote}/${params.defaultBranch}`];
5436
- const addResult = await runGit(addArgs, { cwd: params.repoRoot });
5437
- if (!addResult.ok) {
5438
- return gitErr(describeGitFailure("git worktree add", addResult));
5810
+ if (isAckError(raw)) {
5811
+ return {
5812
+ ok: false,
5813
+ message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043E\u0442\u043A\u0430\u0437\u0430\u043B\u0430 \u043D\u0430 ${event}: ${raw.error.code}: ${raw.error.message}`
5814
+ };
5439
5815
  }
5440
- return gitOk({ worktreeDir, reused: false });
5441
- }
5442
- async function removeWorktree(repoRoot, worktreeDir) {
5443
- const result = await runGit(["worktree", "remove", worktreeDir], { cwd: repoRoot });
5444
- if (!result.ok) {
5445
- return gitErr(describeGitFailure("git worktree remove", result));
5816
+ const parsed = schema.safeParse(raw);
5817
+ if (!parsed.success) {
5818
+ return {
5819
+ ok: false,
5820
+ message: `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043F\u0440\u0438\u0441\u043B\u0430\u043B\u0430 \u043D\u0435\u043E\u0436\u0438\u0434\u0430\u043D\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 \u043D\u0430 ${event}: ${parsed.error.message}`
5821
+ };
5446
5822
  }
5447
- return gitOk(void 0);
5823
+ return { ok: true, data: parsed.data };
5448
5824
  }
5449
5825
 
5450
- // src/git/submit.ts
5451
- async function rebaseOntoDefault(params) {
5452
- const remote = params.remote ?? "origin";
5453
- const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
5454
- if (!defaultBranchCheck.ok) return defaultBranchCheck;
5455
- const fetchResult = await runGit(["fetch", remote, params.defaultBranch], {
5456
- cwd: params.worktreeDir
5457
- });
5458
- if (!fetchResult.ok) {
5459
- return gitErr(describeGitFailure(`git fetch ${remote} ${params.defaultBranch}`, fetchResult));
5460
- }
5461
- const rebaseResult = await runGit(["rebase", `${remote}/${params.defaultBranch}`], {
5462
- cwd: params.worktreeDir
5826
+ // src/mcp/ask.ts
5827
+ var askHumanInputShape = {
5828
+ textMd: questionAskPayloadSchema.shape.textMd.describe(
5829
+ "\u0422\u0435\u043A\u0441\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, Markdown. \u041F\u0438\u0448\u0438 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0431\u0435\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430 \u0441 \u043C\u043E\u0434\u0435\u043B\u044C\u044E."
5830
+ ),
5831
+ targetRole: questionAskPayloadSchema.shape.targetRole.describe(
5832
+ '\u0420\u043E\u043B\u044C \u0430\u0434\u0440\u0435\u0441\u0430\u0442\u0430 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, "owner") \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u0443\u0432\u0438\u0434\u044F\u0442 \u0432\u0441\u0435 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441 \u044D\u0442\u043E\u0439 \u0440\u043E\u043B\u044C\u044E, \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u043F\u0435\u0440\u0432\u044B\u0439. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E.'
5833
+ ),
5834
+ targetMemberId: questionAskPayloadSchema.shape.targetMemberId.describe(
5835
+ "ID \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0430\u0434\u0440\u0435\u0441\u0430\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u041D\u0443\u0436\u0435\u043D \u0420\u041E\u0412\u041D\u041E \u041E\u0414\u0418\u041D \u0430\u0434\u0440\u0435\u0441\u0430\u0442: targetRole \u0418\u041B\u0418 targetMemberId, \u043D\u0435 \u043E\u0431\u0430 \u0441\u0440\u0430\u0437\u0443 \u0438 \u043D\u0435 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E."
5836
+ ),
5837
+ wantsFiles: questionAskPayloadSchema.shape.wantsFiles.describe(
5838
+ "\u041F\u043E\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u043A \u043E\u0442\u0432\u0435\u0442\u0443 (\u0441\u043A\u0440\u0438\u043D\u0448\u043E\u0442, \u043B\u043E\u0433, \u0432\u044B\u0433\u0440\u0443\u0437\u043A\u0443). \u0424\u043E\u0440\u043C\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443 \u0441 \u0432\u044B\u0431\u043E\u0440\u043E\u043C \u0444\u0430\u0439\u043B\u0430. \u042D\u0442\u043E \u041F\u0420\u041E\u0421\u042C\u0411\u0410, \u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435: \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0432\u043F\u0440\u0430\u0432\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u0431\u0435\u0437 \u0444\u0430\u0439\u043B\u043E\u0432, \u0438 \u0442\u043E\u0433\u0434\u0430 `attachments` \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u0431\u0443\u0434\u0435\u0442 \u043F\u0443\u0441\u0442\u044B\u043C."
5839
+ ),
5840
+ options: questionAskPayloadSchema.shape.options.describe(
5841
+ "\u0412\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u043E\u0442\u0432\u0435\u0442\u0430 (\u043F\u043E\u043A\u0430\u0436\u0443\u0442\u0441\u044F \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438 \u0432 UI/Telegram). \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0439 \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043E\u0442\u0432\u0435\u0442\u0438\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C."
5842
+ ),
5843
+ blocking: questionAskPayloadSchema.shape.blocking.describe(
5844
+ 'true \u2014 \u0442\u0432\u043E\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 \u0441\u0442\u0430\u0442\u0443\u0441 WAITING_HUMAN, \u0438 \u044D\u0442\u043E\u0442 \u0432\u044B\u0437\u043E\u0432 \u0441\u0430\u043C \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442 \u0434\u043E 600 \u0441; \u0435\u0441\u043B\u0438 \u0437\u0430 \u044D\u0442\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435\u0442, \u0432\u044B\u0437\u043E\u0432 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0441 \u043F\u043E\u043C\u0435\u0442\u043A\u043E\u0439 "pending" \u2014 \u0442\u043E\u0433\u0434\u0430 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430, \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430. false \u2014 \u0432\u043E\u043F\u0440\u043E\u0441 \u043F\u0440\u043E\u0441\u0442\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F, \u0431\u0435\u0437 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0438 \u0431\u0435\u0437 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 (\u0434\u043B\u044F \u043D\u0435 \u0441\u0440\u043E\u0447\u043D\u044B\u0445 \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0439).'
5845
+ )
5846
+ };
5847
+ var BLOCKING_WAIT_SEC = 600;
5848
+ var ASK_ACK_BUFFER_MS = 15e3;
5849
+ async function askHumanHandler(args, runtime) {
5850
+ const state = readState(runtime.stateFilePath);
5851
+ const taskId = state.currentTask?.taskId;
5852
+ const waitSec = args.blocking ? BLOCKING_WAIT_SEC : 0;
5853
+ const parsedArgs = parseArgsOrError(questionAskPayloadSchema, {
5854
+ ...taskId ? { taskId } : {},
5855
+ textMd: args.textMd,
5856
+ ...args.targetRole ? { targetRole: args.targetRole } : {},
5857
+ ...args.targetMemberId ? { targetMemberId: args.targetMemberId } : {},
5858
+ ...args.options && args.options.length > 0 ? { options: args.options } : {},
5859
+ ...args.wantsFiles ? { wantsFiles: true } : {},
5860
+ blocking: args.blocking,
5861
+ waitSec
5463
5862
  });
5464
- if (rebaseResult.ok) {
5465
- return gitOk({ outcome: "rebased", conflictedFiles: [] });
5466
- }
5467
- const status = await getWorktreeStatus(params.worktreeDir);
5468
- if (status.ok && status.data.conflictedPaths.length > 0) {
5469
- const conflictedFiles = status.data.conflictedPaths;
5470
- const abortResult = await runGit(["rebase", "--abort"], { cwd: params.worktreeDir });
5471
- if (!abortResult.ok) {
5472
- return gitErr(
5473
- `\u041A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043F\u0440\u0438 rebase (${conflictedFiles.join(", ")}), \u0438 "git rebase --abort" \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u043B: ${describeGitFailure("git rebase --abort", abortResult)}. Worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u043D\u043E \u0440\u0443\u0447\u043D\u043E\u0435 \u0432\u043C\u0435\u0448\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E.`
5474
- );
5475
- }
5476
- return gitOk({ outcome: "conflict", conflictedFiles });
5477
- }
5478
- return gitErr(describeGitFailure("git rebase", rebaseResult));
5479
- }
5480
- async function assertReadyToPush(params) {
5481
- const branch = await currentBranch(params.worktreeDir);
5482
- if (!branch.ok) return branch;
5483
- if (branch.data !== params.branchName) {
5484
- return gitErr(
5485
- `\u041E\u0442\u043A\u0430\u0437: worktree \u0441\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u0432\u0435\u0442\u043A\u0443 "${branch.data}", \u0430 \u043E\u0436\u0438\u0434\u0430\u043B\u0430\u0441\u044C "${params.branchName}" \u2014 \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E "\u0443\u0448\u043B\u043E \u043D\u0435 \u0442\u0443\u0434\u0430", push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
5486
- );
5487
- }
5488
- const status = await getWorktreeStatus(params.worktreeDir);
5489
- if (!status.ok) return status;
5490
- if (!status.data.clean) {
5491
- return gitErr(
5492
- `\u041E\u0442\u043A\u0430\u0437: \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E \u0433\u0440\u044F\u0437\u043D\u043E\u0435, \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u043F\u0443\u0442\u0438: ${status.data.changedPaths.join(", ")}. Push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F.`
5493
- );
5863
+ if (!parsedArgs.ok) return errorResult(parsedArgs.message);
5864
+ if (taskId) {
5865
+ const lease = runtime.client.isLeaseLost(taskId);
5866
+ if (lease.lost) return leaseLostResult(state.currentTask?.taskKey ?? taskId, lease.reason);
5494
5867
  }
5495
- return gitOk(void 0);
5496
- }
5497
- async function pushFastForward(params) {
5498
- const remote = params.remote ?? "origin";
5499
- const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
5500
- if (!branchCheck.ok) return branchCheck;
5501
- const defaultBranchCheck = requireValidBranchName(params.defaultBranch, "\u0418\u043C\u044F \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0432\u0435\u0442\u043A\u0438");
5502
- if (!defaultBranchCheck.ok) return defaultBranchCheck;
5503
- const ready = await assertReadyToPush(params);
5504
- if (!ready.ok) return ready;
5505
- const pushResult = await runGit(["push", remote, `HEAD:refs/heads/${params.defaultBranch}`], {
5506
- cwd: params.worktreeDir
5507
- });
5508
- if (!pushResult.ok) {
5509
- return gitErr(
5510
- describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.defaultBranch}`, pushResult)
5868
+ const result = await callServerOrError(
5869
+ runtime.client,
5870
+ "question:ask",
5871
+ parsedArgs.data,
5872
+ questionAskAckSchema,
5873
+ waitSec * 1e3 + ASK_ACK_BUFFER_MS
5874
+ );
5875
+ if (!result.ok) return errorResult(result.message);
5876
+ const settled = describeSettledQuestion(result.data.question);
5877
+ if (settled) return textResult(settled);
5878
+ if (args.blocking) {
5879
+ return textResult(
5880
+ `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0437\u0430\u0434\u0430\u043D, \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043D\u0430 \u0432 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 (WAITING_HUMAN). \u041E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E ${BLOCKING_WAIT_SEC} \u0441 (pending) \u2014 \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}", \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0436\u0434\u0430\u0442\u044C. \u041D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443 \u043F\u043E \u0437\u0430\u0434\u0430\u0447\u0435, \u043F\u043E\u043A\u0430 \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u0448\u044C \u043E\u0442\u0432\u0435\u0442.`
5511
5881
  );
5512
5882
  }
5513
- return gitOk({ pushedTo: `${remote}/${params.defaultBranch}` });
5883
+ return textResult(
5884
+ `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D (\u043D\u0435 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443). \u041E\u0442\u0432\u0435\u0442\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u2014 \u0435\u0441\u043B\u0438 \u043E\u043D \u043D\u0443\u0436\u0435\u043D, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C, \u0432\u044B\u0437\u043E\u0432\u0438 wait_answer \u0441 questionId="${result.data.question.id}".`
5885
+ );
5514
5886
  }
5515
- async function submitDirectTask(params) {
5516
- const remote = params.remote ?? "origin";
5517
- let worktreeDir;
5518
- if (params.worktreeDir) {
5519
- worktreeDir = params.worktreeDir;
5520
- } else {
5521
- const worktree = await ensureWorktree({
5522
- repoRoot: params.repoRoot,
5523
- branchName: params.branchName,
5524
- defaultBranch: params.defaultBranch,
5525
- remote,
5526
- worktreesRoot: params.worktreesRoot
5527
- });
5528
- if (!worktree.ok) return { status: "refused", message: worktree.message };
5529
- worktreeDir = worktree.data.worktreeDir;
5530
- }
5531
- const commit = await commitAll({
5532
- worktreeDir,
5533
- commitPrefix: params.commitPrefix,
5534
- title: params.taskTitle,
5535
- taskKey: params.taskKey
5536
- });
5537
- if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
5538
- const rebase = await rebaseOntoDefault({
5539
- worktreeDir,
5540
- defaultBranch: params.defaultBranch,
5541
- remote
5887
+ var waitAnswerInputShape = {
5888
+ questionId: questionWaitPayloadSchema.shape.questionId.describe(
5889
+ "ID \u0432\u043E\u043F\u0440\u043E\u0441\u0430 \u2014 \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430 ask_human (question.id) \u0438\u043B\u0438 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0433\u043E \u0432\u044B\u0437\u043E\u0432\u0430 wait_answer."
5890
+ ),
5891
+ waitSec: questionWaitPayloadSchema.shape.waitSec.optional().describe("\u0421\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0435\u043A\u0443\u043D\u0434 \u0436\u0434\u0430\u0442\u044C \u043E\u0442\u0432\u0435\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u044B\u0437\u043E\u0432\u0435, 0..600. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E 300.")
5892
+ };
5893
+ var DEFAULT_WAIT_ANSWER_SEC = 300;
5894
+ var WAIT_ANSWER_ACK_BUFFER_MS = 15e3;
5895
+ async function waitAnswerHandler(args, runtime) {
5896
+ const waitSec = args.waitSec ?? DEFAULT_WAIT_ANSWER_SEC;
5897
+ const parsedArgs = parseArgsOrError(questionWaitPayloadSchema, {
5898
+ questionId: args.questionId,
5899
+ waitSec
5542
5900
  });
5543
- if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
5544
- if (rebase.data.outcome === "conflict") {
5545
- const files = rebase.data.conflictedFiles;
5546
- return {
5547
- status: "conflict",
5548
- worktreeDir,
5549
- conflictedFiles: files,
5550
- message: `Rebase \u043D\u0430 ${remote}/${params.defaultBranch} \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0432 \u0444\u0430\u0439\u043B\u0430\u0445: ${files.join(", ")}. Rebase \u043E\u0442\u043C\u0435\u043D\u0451\u043D ("git rebase --abort"), \u0432\u0435\u0442\u043A\u0430 "${params.branchName}" \u0438 \u0435\u0451 worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`
5551
- };
5552
- }
5553
- if (params.requireTestsPassed && !params.testsPassed) {
5554
- return {
5555
- status: "refused",
5556
- worktreeDir,
5557
- message: "\u0414\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 requireTests=true, \u0430 testsPassed \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D (\u0438\u043B\u0438 false) \u2014 push \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F."
5558
- };
5901
+ if (!parsedArgs.ok) return errorResult(parsedArgs.message);
5902
+ const result = await callServerOrError(
5903
+ runtime.client,
5904
+ "question:wait",
5905
+ parsedArgs.data,
5906
+ questionWaitAckSchema,
5907
+ waitSec * 1e3 + WAIT_ANSWER_ACK_BUFFER_MS
5908
+ );
5909
+ if (!result.ok) return errorResult(result.message);
5910
+ const settled = describeSettledQuestion(result.data.question);
5911
+ if (settled) return textResult(settled);
5912
+ return textResult(
5913
+ `\u0412\u043E\u043F\u0440\u043E\u0441 ${result.data.question.id} \u0432\u0441\u0451 \u0435\u0449\u0451 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 (\u0436\u0434\u0430\u043B ${waitSec} \u0441). \u0412\u044B\u0437\u043E\u0432\u0438 wait_answer \u0435\u0449\u0451 \u0440\u0430\u0437 \u0441 \u0442\u0435\u043C \u0436\u0435 questionId, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0443\u043C\u0435\u0441\u0442\u043D\u043E.`
5914
+ );
5915
+ }
5916
+ function describeAnswerAttachments(question) {
5917
+ if (question.attachments.length === 0) return "";
5918
+ const lines = question.attachments.map(
5919
+ (file) => file.downloadUrl ? `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u043A\u0430\u0447\u0430\u0439: ${file.downloadUrl}` : `- ${file.fileName} (${file.mimeType}) \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430, \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442`
5920
+ );
5921
+ return `
5922
+
5923
+ \u041A \u043E\u0442\u0432\u0435\u0442\u0443 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u044B \u0444\u0430\u0439\u043B\u044B (${question.attachments.length}). \u042D\u0442\u043E \u0447\u0430\u0441\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u0430: \u0441\u043A\u0430\u0447\u0430\u0439 \u0438 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u041A\u0410\u0416\u0414\u042B\u0419, \u0441\u0441\u044B\u043B\u043A\u0438 \u0436\u0438\u0432\u0443\u0442 ${question.attachments[0]?.expiresInSeconds ?? 300} \u0441 \u2014 \u043D\u0435 \u043E\u0442\u043A\u043B\u0430\u0434\u044B\u0432\u0430\u0439.
5924
+ ` + lines.join("\n");
5925
+ }
5926
+ function describeSettledQuestion(question) {
5927
+ if (question.status === QuestionStatus.ANSWERED) {
5928
+ return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043F\u043E\u043B\u0443\u0447\u0438\u043B \u043E\u0442\u0432\u0435\u0442:
5929
+ ${question.answerMd ?? ""}${describeAnswerAttachments(question)}`;
5559
5930
  }
5560
- const push = await pushFastForward({
5561
- worktreeDir,
5562
- branchName: params.branchName,
5563
- defaultBranch: params.defaultBranch,
5564
- remote
5565
- });
5566
- if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
5567
- const sha = await getHeadSha(worktreeDir);
5568
- let worktreeRemoved = false;
5569
- if (params.removeWorktreeOnSuccess !== false) {
5570
- const removed = await removeWorktree(params.repoRoot, worktreeDir);
5571
- if (removed.ok) {
5572
- await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
5573
- worktreeRemoved = true;
5574
- }
5931
+ if (question.status === QuestionStatus.DISMISSED) {
5932
+ return `\u0412\u043E\u043F\u0440\u043E\u0441 ${question.id} \u043E\u0442\u043A\u043B\u043E\u043D\u0451\u043D (DISMISSED) \u2014 \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442. \u0420\u0435\u0448\u0438, \u043A\u0430\u043A \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043B\u044C\u0448\u0435, \u0441\u0430\u043C, \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0437\u0430\u043D\u043E\u0432\u043E.`;
5575
5933
  }
5576
- return {
5577
- status: "pushed",
5578
- worktreeDir,
5579
- commitSha: sha.ok ? sha.data : null,
5580
- pushedTo: push.data.pushedTo,
5581
- worktreeRemoved
5582
- };
5934
+ return void 0;
5583
5935
  }
5584
5936
 
5585
- // src/git/submit-pr.ts
5586
- async function pushTaskBranch(params) {
5587
- const remote = params.remote ?? "origin";
5588
- const branchCheck = requireValidBranchName(params.branchName, "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438");
5589
- if (!branchCheck.ok) return branchCheck;
5590
- const ready = await assertReadyToPush(params);
5591
- if (!ready.ok) return ready;
5592
- const pushResult = await runGit(
5593
- ["push", "--set-upstream", remote, `HEAD:refs/heads/${params.branchName}`],
5594
- { cwd: params.worktreeDir }
5937
+ // package.json
5938
+ var package_default = {
5939
+ name: "@quiel/cli",
5940
+ version: "0.6.0",
5941
+ description: "CLI \u0438 MCP-\u0441\u0435\u0440\u0432\u0435\u0440 Quiel: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0430\u0433\u0435\u043D\u0442\u0430 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430 (Claude Code, Codex, Cursor, OpenCode) \u043A \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0435",
5942
+ homepage: "https://quiel.app",
5943
+ repository: {
5944
+ type: "git",
5945
+ url: "git+https://github.com/Quiel-App/quiel-cli.git"
5946
+ },
5947
+ bugs: {
5948
+ url: "https://github.com/Quiel-App/quiel-cli/issues"
5949
+ },
5950
+ keywords: [
5951
+ "quiel",
5952
+ "claude-code",
5953
+ "codex",
5954
+ "cursor",
5955
+ "opencode",
5956
+ "mcp",
5957
+ "mcp-server",
5958
+ "ai-agents",
5959
+ "task-tracker"
5960
+ ],
5961
+ license: "Apache-2.0",
5962
+ type: "module",
5963
+ bin: {
5964
+ quiel: "dist/cli.js"
5965
+ },
5966
+ files: [
5967
+ "dist",
5968
+ "README.md",
5969
+ "LICENSE"
5970
+ ],
5971
+ engines: {
5972
+ node: ">=22"
5973
+ },
5974
+ publishConfig: {
5975
+ access: "public"
5976
+ },
5977
+ scripts: {
5978
+ build: "tsup",
5979
+ dev: "tsup --watch",
5980
+ lint: "biome check .",
5981
+ typecheck: "tsc --noEmit",
5982
+ test: "vitest run --passWithNoTests"
5983
+ },
5984
+ dependencies: {
5985
+ "@modelcontextprotocol/sdk": "1.30.0",
5986
+ commander: "15.0.0",
5987
+ "socket.io-client": "4.8.3",
5988
+ zod: "4.5.4"
5989
+ },
5990
+ devDependencies: {
5991
+ "@quiel/config": "workspace:*",
5992
+ "@quiel/shared": "workspace:*",
5993
+ "@types/node": "20.19.43",
5994
+ tsup: "8.5.1",
5995
+ typescript: "7.0.2",
5996
+ vitest: "5.0.0"
5997
+ }
5998
+ };
5999
+
6000
+ // src/version.ts
6001
+ var VERSION = package_default.version;
6002
+
6003
+ // src/mcp/constants.ts
6004
+ var MCP_SERVER_NAME = "quiel";
6005
+ var MCP_SERVER_VERSION = VERSION;
6006
+
6007
+ // src/mcp/get-context.ts
6008
+ var ALL_CONTEXT_INCLUDES = [
6009
+ "spec",
6010
+ "decisions",
6011
+ // ВСЕ документы проекта — по умолчанию, а не по особому запросу.
6012
+ // `spec`/`decisions` покрывают два типа из пяти, и человек, положивший
6013
+ // контекст под типом `DATA` или `PLAN`, вправе ожидать, что агент его
6014
+ // увидит. 14 сентября 2026 ровно этого и не случилось.
6015
+ "documents",
6016
+ "related",
6017
+ "repo"
6018
+ ];
6019
+ var getContextInputShape = {
6020
+ taskId: contextGetPayloadSchema.shape.taskId.describe(
6021
+ "ID \u0437\u0430\u0434\u0430\u0447\u0438, \u0434\u043B\u044F \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u043D\u0443\u0436\u0435\u043D \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442. \u0415\u0441\u043B\u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u0430\u0433\u0435\u043D\u0442\u0430 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C)."
6022
+ ),
6023
+ include: contextGetPayloadSchema.shape.include.optional().describe(
6024
+ "\u0427\u0442\u043E \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C: spec | decisions | related | repo. \u041D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u0438\u043B\u0438 \u043F\u0443\u0441\u0442 \u2014 \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0432\u0441\u0451 \u0441\u0440\u0430\u0437\u0443."
6025
+ )
6026
+ };
6027
+ async function getContextHandler(args, runtime) {
6028
+ const state = readState(runtime.stateFilePath);
6029
+ const taskId = args.taskId ?? state.currentTask?.taskId;
6030
+ if (taskId) {
6031
+ const lease = runtime.client.isLeaseLost(taskId);
6032
+ if (lease.lost) return leaseLostResult(taskId, lease.reason);
6033
+ }
6034
+ const include = args.include && args.include.length > 0 ? args.include : [...ALL_CONTEXT_INCLUDES];
6035
+ const payload = contextGetPayloadSchema.parse({ ...taskId ? { taskId } : {}, include });
6036
+ const result = await callServerOrError(
6037
+ runtime.client,
6038
+ "context:get",
6039
+ payload,
6040
+ contextPayloadSchema
5595
6041
  );
5596
- if (!pushResult.ok) {
5597
- return gitErr(
5598
- describeGitFailure(`git push ${remote} HEAD:refs/heads/${params.branchName}`, pushResult)
6042
+ if (!result.ok) return errorResult(result.message);
6043
+ return textResult(JSON.stringify(result.data, null, 2));
6044
+ }
6045
+
6046
+ // src/mcp/get-task.ts
6047
+ var getTaskInputShape = {
6048
+ taskId: taskGetPayloadSchema.shape.taskId.describe("ID \u0437\u0430\u0434\u0430\u0447\u0438 (Task.id) \u2014 \u043D\u0435 \u043A\u043B\u044E\u0447 \u0432\u0438\u0434\u0430 REL-42")
6049
+ };
6050
+ async function getTaskHandler(args, runtime) {
6051
+ const lease = runtime.client.isLeaseLost(args.taskId);
6052
+ if (lease.lost) return leaseLostResult(args.taskId, lease.reason);
6053
+ const payload = taskGetPayloadSchema.parse({ taskId: args.taskId });
6054
+ const result = await callServerOrError(runtime.client, "task:get", payload, taskGetAckSchema);
6055
+ if (!result.ok) return errorResult(result.message);
6056
+ return textResult(JSON.stringify(result.data, null, 2));
6057
+ }
6058
+
6059
+ // src/mcp/log.ts
6060
+ var logInputShape = {
6061
+ message: taskProgressPayloadSchema.shape.note.describe("\u0421\u0442\u0440\u043E\u043A\u0430 \u0432 \u043B\u0435\u043D\u0442\u0443 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438"),
6062
+ level: taskProgressPayloadSchema.shape.level.optional().describe("debug | info | warn | error, \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E info")
6063
+ };
6064
+ async function logHandler(args, runtime) {
6065
+ const state = readState(runtime.stateFilePath);
6066
+ if (!state.currentTask) {
6067
+ return errorResult(
6068
+ "\u041D\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0437\u0430\u043F\u0438\u0441\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430 \u0432 \u0435\u0451 \u043B\u0435\u043D\u0442\u0443. \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0437\u043E\u0432\u0438 next_task."
5599
6069
  );
5600
6070
  }
5601
- return gitOk({ pushedTo: `${remote}/${params.branchName}` });
5602
- }
5603
- async function submitPullRequestTask(params) {
5604
- const remote = params.remote ?? "origin";
5605
- let worktreeDir;
5606
- if (params.worktreeDir) {
5607
- worktreeDir = params.worktreeDir;
5608
- } else {
5609
- const worktree = await ensureWorktree({
5610
- repoRoot: params.repoRoot,
5611
- branchName: params.branchName,
5612
- defaultBranch: params.defaultBranch,
5613
- remote,
5614
- worktreesRoot: params.worktreesRoot
5615
- });
5616
- if (!worktree.ok) return { status: "refused", message: worktree.message };
5617
- worktreeDir = worktree.data.worktreeDir;
6071
+ const lease = runtime.client.isLeaseLost(state.currentTask.taskId);
6072
+ if (lease.lost) {
6073
+ return leaseLostResult(state.currentTask.taskKey ?? state.currentTask.taskId, lease.reason);
5618
6074
  }
5619
- const commit = await commitAll({
5620
- worktreeDir,
5621
- commitPrefix: params.commitPrefix,
5622
- title: params.taskTitle,
5623
- taskKey: params.taskKey
5624
- });
5625
- if (!commit.ok) return { status: "refused", worktreeDir, message: commit.message };
5626
- const rebase = await rebaseOntoDefault({
5627
- worktreeDir,
5628
- defaultBranch: params.defaultBranch,
5629
- remote
6075
+ const payload = taskProgressPayloadSchema.parse({
6076
+ taskId: state.currentTask.taskId,
6077
+ note: args.message,
6078
+ ...args.level ? { level: args.level } : {}
5630
6079
  });
5631
- if (!rebase.ok) return { status: "refused", worktreeDir, message: rebase.message };
5632
- const conflicted = rebase.data.outcome === "conflict";
5633
- const push = await pushTaskBranch({ worktreeDir, branchName: params.branchName, remote });
5634
- if (!push.ok) return { status: "refused", worktreeDir, message: push.message };
5635
- const sha = await getHeadSha(worktreeDir);
5636
- let worktreeRemoved = false;
5637
- if (params.removeWorktreeOnSuccess !== false) {
5638
- const removed = await removeWorktree(params.repoRoot, worktreeDir);
5639
- if (removed.ok) {
5640
- await runGit(["branch", "-D", params.branchName], { cwd: params.repoRoot });
5641
- worktreeRemoved = true;
5642
- }
5643
- }
5644
- return {
5645
- status: "pushed",
5646
- worktreeDir,
5647
- branchName: params.branchName,
5648
- commitSha: sha.ok ? sha.data : null,
5649
- pushedTo: push.data.pushedTo,
5650
- conflicted,
5651
- conflictedFiles: rebase.data.conflictedFiles,
5652
- worktreeRemoved
5653
- };
6080
+ const result = await callServerOrError(
6081
+ runtime.client,
6082
+ "task:progress",
6083
+ payload,
6084
+ taskProgressAckSchema
6085
+ );
6086
+ if (!result.ok) return errorResult(result.message);
6087
+ if (!result.data.ok) return errorResult("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u0437\u0430\u043F\u0438\u0441\u044C \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 (ok:false).");
6088
+ return textResult("\u0417\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u043B\u0435\u043D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0438.");
5654
6089
  }
5655
6090
 
5656
6091
  // src/mcp/task-worktree.ts
@@ -5688,7 +6123,7 @@ var nextTaskInputShape = {
5688
6123
  "\u0421\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0435\u043A\u0443\u043D\u0434 \u0436\u0434\u0430\u0442\u044C \u043F\u043E\u044F\u0432\u043B\u0435\u043D\u0438\u044F \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, 0..600 (\xA711.1 \u0422\u0417). \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E 300 \u2014 \u043A\u0430\u043A claimTimeoutSec \u0434\u0438\u0441\u043F\u0435\u0442\u0447\u0435\u0440\u0430 (\xA79.1 \u0422\u0417)."
5689
6124
  )
5690
6125
  };
5691
- var DEFAULT_WAIT_SEC = 300;
6126
+ var DEFAULT_WAIT_SEC = 90;
5692
6127
  var CLAIM_ACK_BUFFER_MS = 15e3;
5693
6128
  async function nextTaskHandler(args, runtime) {
5694
6129
  const { client, stateFilePath } = runtime;
@@ -5714,8 +6149,9 @@ async function nextTaskHandler(args, runtime) {
5714
6149
  );
5715
6150
  if (!claimed.ok) return errorResult(claimed.message);
5716
6151
  if ("empty" in claimed.data) {
6152
+ updateState({ queueEmptyAt: (/* @__PURE__ */ new Date()).toISOString() }, stateFilePath);
5717
6153
  return textResult(
5718
- `\u0417\u0430\u0434\u0430\u0447 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442 (\u0436\u0434\u0430\u043B ${waitSec} \u0441). \u042D\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0438\u0441\u0445\u043E\u0434, \u043D\u0435 \u043E\u0448\u0438\u0431\u043A\u0430 \u2014 \u0432\u044B\u0437\u043E\u0432\u0438 next_task \u0435\u0449\u0451 \u0440\u0430\u0437, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0443\u043C\u0435\u0441\u0442\u043D\u043E.`
6154
+ `\u0417\u0430\u0434\u0430\u0447 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442 (\u0436\u0434\u0430\u043B ${waitSec} \u0441). \u042D\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0438\u0441\u0445\u043E\u0434, \u043D\u0435 \u043E\u0448\u0438\u0431\u043A\u0430. \u041C\u043E\u0436\u0435\u0448\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0445\u043E\u0434 \u2014 \u0445\u0443\u043A \u0442\u0435\u0431\u044F \u043D\u0435 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442, \u043F\u043E\u043A\u0430 \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043F\u0443\u0441\u0442\u0430.`
5719
6155
  );
5720
6156
  }
5721
6157
  const task = claimed.data;
@@ -5725,8 +6161,13 @@ async function nextTaskHandler(args, runtime) {
5725
6161
  taskId: task.task.id,
5726
6162
  taskKey: task.task.key,
5727
6163
  resumeToken: task.lease.resumeToken,
5728
- gitMode: task.project.gitMode
5729
- }
6164
+ gitMode: task.project.gitMode,
6165
+ branchName: task.project.branchName,
6166
+ commitPrefix: task.project.commitPrefix
6167
+ },
6168
+ // Очередь оказалась не пуста — пометка снимается, иначе хук
6169
+ // продолжал бы считать, что работы нет.
6170
+ queueEmptyAt: null
5730
6171
  },
5731
6172
  stateFilePath
5732
6173
  );
@@ -5738,7 +6179,12 @@ async function nextTaskHandler(args, runtime) {
5738
6179
  const worktree = await ensureWorktree({
5739
6180
  repoRoot: runtime.cwd,
5740
6181
  branchName: plan.branchName,
5741
- defaultBranch: plan.defaultBranch
6182
+ defaultBranch: plan.defaultBranch,
6183
+ // LIM-03: задачу уже начинали и прервали — рабочее дерево заводим
6184
+ // ОТ ТОЙ ВЕТКИ, а не от основной. Иначе спасённая работа осталась
6185
+ // бы лежать на origin никем не прочитанной, и агент сделал бы её
6186
+ // заново, потратив свой лимит подписки на чужую.
6187
+ continueFromBranch: task.continueFrom?.branchName
5742
6188
  });
5743
6189
  if (worktree.ok) {
5744
6190
  updateState(
@@ -5748,6 +6194,8 @@ async function nextTaskHandler(args, runtime) {
5748
6194
  taskKey: task.task.key,
5749
6195
  resumeToken: task.lease.resumeToken,
5750
6196
  gitMode: task.project.gitMode,
6197
+ branchName: task.project.branchName,
6198
+ commitPrefix: task.project.commitPrefix,
5751
6199
  worktreeDir: worktree.data.worktreeDir
5752
6200
  }
5753
6201
  },
@@ -5766,11 +6214,84 @@ async function nextTaskHandler(args, runtime) {
5766
6214
  const warning = !started.ok ? `
5767
6215
 
5768
6216
  \u0412\u041D\u0418\u041C\u0410\u041D\u0418\u0415: task:start \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C (${started.message}). \u0410\u0440\u0435\u043D\u0434\u0430 \u0442\u0432\u043E\u044F (\u0437\u0430\u0434\u0430\u0447\u0430 \u0432\u044B\u0434\u0430\u043D\u0430), \u043D\u043E \u0441\u0442\u0430\u0442\u0443\u0441 \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043C\u043E\u0436\u0435\u0442 \u043E\u0441\u0442\u0430\u0442\u044C\u0441\u044F ASSIGNED \u0432\u043C\u0435\u0441\u0442\u043E IN_PROGRESS \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043A\u0430\u043A \u043E\u0431\u044B\u0447\u043D\u043E, report \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u0441\u0434\u0430\u0441\u0442 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442.` : !started.data.ok ? "\n\n\u0412\u041D\u0418\u041C\u0410\u041D\u0418\u0415: \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B task:start (ok:false). \u0410\u0440\u0435\u043D\u0434\u0430 \u0442\u0432\u043E\u044F, \u043D\u043E \u0441\u0442\u0430\u0442\u0443\u0441 \u043C\u043E\u0436\u0435\u0442 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0442\u044C \u2014 \u0443\u043F\u043E\u043C\u044F\u043D\u0438 \u044D\u0442\u043E \u0432 report." : "";
6217
+ const continueNote = task.continueFrom ? `
6218
+
6219
+ \u042D\u0422\u0423 \u0417\u0410\u0414\u0410\u0427\u0423 \u0423\u0416\u0415 \u041D\u0410\u0427\u0418\u041D\u0410\u041B\u0418. \u041D\u0435\u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u043B\u0435\u0436\u0438\u0442 \u0432 \u0432\u0435\u0442\u043A\u0435 "${task.continueFrom.branchName}"${task.continueFrom.sha ? ` (\u043A\u043E\u043C\u043C\u0438\u0442 ${task.continueFrom.sha.slice(0, 8)})` : ""}${task.continueFrom.note ? `: ${task.continueFrom.note}` : ""}. \u0420\u0430\u0431\u043E\u0447\u0435\u0435 \u0434\u0435\u0440\u0435\u0432\u043E \u0437\u0430\u0432\u0435\u0434\u0435\u043D\u043E \u043E\u0442 \u043D\u0435\u0451 \u2014 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E, \u0438 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439, \u0430 \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0439 \u0437\u0430\u043D\u043E\u0432\u043E. \u0420\u0435\u0448\u0438\u0448\u044C, \u0447\u0442\u043E \u043F\u0440\u043E\u0449\u0435 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0442\u044C, \u2014 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043D\u043E \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438.` : "";
5769
6220
  return textResult(
5770
6221
  `\u0417\u0430\u0434\u0430\u0447\u0430 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0430:
5771
6222
  ${JSON.stringify(task, null, 2)}
5772
6223
 
5773
- ${worktreeNote}${warning}`
6224
+ ${worktreeNote}${continueNote}${warning}`
6225
+ );
6226
+ }
6227
+
6228
+ // src/mcp/link.ts
6229
+ var linkTasksInputShape = {
6230
+ blockerId: taskLinkPayloadSchema.shape.blockerId.describe(
6231
+ "ID \u0437\u0430\u0434\u0430\u0447\u0438, \u043A\u043E\u0442\u043E\u0440\u0443\u044E \u043D\u0430\u0434\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u041F\u0415\u0420\u0412\u041E\u0419 \u2014 \u043E\u043D\u0430 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0432\u0442\u043E\u0440\u0443\u044E."
6232
+ ),
6233
+ blockedId: taskLinkPayloadSchema.shape.blockedId.describe(
6234
+ "ID \u0437\u0430\u0434\u0430\u0447\u0438, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0416\u0414\u0401\u0422: \u0435\u0451 \u043D\u0435\u043B\u044C\u0437\u044F \u0431\u0440\u0430\u0442\u044C, \u043F\u043E\u043A\u0430 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0442\u0430 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0430\u044F."
6235
+ ),
6236
+ kind: taskLinkPayloadSchema.shape.kind.describe(
6237
+ "BLOCKS \u2014 \u0434\u0435\u0440\u0436\u0438\u0442 \u043E\u0447\u0435\u0440\u0435\u0434\u044C (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E). RELATES \u2014 \u043F\u0440\u043E\u0441\u0442\u043E \u0441\u0432\u044F\u0437\u044C \xAB\u0441\u043C. \u0442\u0430\u043A\u0436\u0435\xBB, \u043D\u0430 \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043D\u0435 \u0432\u043B\u0438\u044F\u0435\u0442."
6238
+ )
6239
+ };
6240
+ async function linkTasksHandler(args, runtime) {
6241
+ const parsedArgs = parseArgsOrError(taskLinkPayloadSchema, args);
6242
+ if (!parsedArgs.ok) return errorResult(parsedArgs.message);
6243
+ const result = await callServerOrError(
6244
+ runtime.client,
6245
+ "task:link",
6246
+ parsedArgs.data,
6247
+ taskLinkAckSchema
6248
+ );
6249
+ if (!result.ok) return errorResult(result.message);
6250
+ if (!result.data.linked) {
6251
+ return textResult(`\u0421\u0432\u044F\u0437\u044C \u0443\u0436\u0435 \u0431\u044B\u043B\u0430: ${args.blockerId} \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 ${args.blockedId}.`);
6252
+ }
6253
+ const stopped = result.data.blockedNow ? " \u0417\u0430\u0434\u0430\u0447\u0430 \u0431\u044B\u043B\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435 \u0438 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u043E\u043D\u0430 \u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0443\u044E." : "";
6254
+ return textResult(`\u0413\u043E\u0442\u043E\u0432\u043E: ${args.blockerId} \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 ${args.blockedId}.${stopped}`);
6255
+ }
6256
+ var unlinkTasksInputShape = {
6257
+ blockerId: taskUnlinkPayloadSchema.shape.blockerId.describe("ID \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438."),
6258
+ blockedId: taskUnlinkPayloadSchema.shape.blockedId.describe("ID \u0436\u0434\u0443\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438.")
6259
+ };
6260
+ async function unlinkTasksHandler(args, runtime) {
6261
+ const parsedArgs = parseArgsOrError(taskUnlinkPayloadSchema, args);
6262
+ if (!parsedArgs.ok) return errorResult(parsedArgs.message);
6263
+ const result = await callServerOrError(
6264
+ runtime.client,
6265
+ "task:unlink",
6266
+ parsedArgs.data,
6267
+ taskUnlinkAckSchema
6268
+ );
6269
+ if (!result.ok) return errorResult(result.message);
6270
+ return textResult(
6271
+ result.data.unlinked ? `\u0421\u0432\u044F\u0437\u044C \u0441\u043D\u044F\u0442\u0430: ${args.blockerId} \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 ${args.blockedId}.` : `\u0422\u0430\u043A\u043E\u0439 \u0441\u0432\u044F\u0437\u0438 \u043D\u0435 \u0431\u044B\u043B\u043E: ${args.blockerId} \u043D\u0435 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043B ${args.blockedId}.`
6272
+ );
6273
+ }
6274
+ var planTaskInputShape = {
6275
+ taskId: taskPlanPayloadSchema.shape.taskId.describe(
6276
+ "ID \u0437\u0430\u0434\u0430\u0447\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u043B\u044E\u0431\u043E\u0439, \u0430\u0440\u0435\u043D\u0434\u0430 \u043D\u0435 \u043D\u0443\u0436\u043D\u0430."
6277
+ ),
6278
+ patch: taskPlanPayloadSchema.shape.patch.describe(
6279
+ "\u041F\u043E\u043B\u044F \u043F\u043B\u0430\u043D\u0430: title, descriptionMd, acceptance, type, priority, roleKey, assigneeId, milestoneId, labels, estimateMin. \u0421\u0442\u0430\u0442\u0443\u0441 \u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u0440\u0430\u0431\u043E\u0442\u044B \u043C\u0435\u043D\u044F\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F \u2014 \u0441\u0442\u0430\u0442\u0443\u0441 \u043F\u0435\u0440\u0435\u0432\u043E\u0434\u0438\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A, \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u043F\u0440\u0438\u0445\u043E\u0434\u044F\u0442 \u0438\u0437 report."
6280
+ )
6281
+ };
6282
+ async function planTaskHandler(args, runtime) {
6283
+ const parsedArgs = parseArgsOrError(taskPlanPayloadSchema, args);
6284
+ if (!parsedArgs.ok) return errorResult(parsedArgs.message);
6285
+ const result = await callServerOrError(
6286
+ runtime.client,
6287
+ "task:plan",
6288
+ parsedArgs.data,
6289
+ taskPlanAckSchema
6290
+ );
6291
+ if (!result.ok) return errorResult(result.message);
6292
+ return textResult(
6293
+ `\u0417\u0430\u0434\u0430\u0447\u0430 ${result.data.task.key} \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430:
6294
+ ${JSON.stringify(result.data.task, null, 2)}`
5774
6295
  );
5775
6296
  }
5776
6297
 
@@ -5850,8 +6371,15 @@ async function reportHandler(args, runtime) {
5850
6371
  );
5851
6372
  }
5852
6373
  const taskLabel2 = state.currentTask.taskKey ?? state.currentTask.taskId;
6374
+ const completedTaskIds = state.completedTaskIds.includes(state.currentTask.taskId) ? state.completedTaskIds : [...state.completedTaskIds, state.currentTask.taskId];
5853
6375
  updateState(
5854
- { currentTask: null, tasksCompletedInSession: state.tasksCompletedInSession + 1 },
6376
+ {
6377
+ currentTask: null,
6378
+ tasksCompletedInSession: state.tasksCompletedInSession + 1,
6379
+ completedTaskIds,
6380
+ // Сдали задачу — значит очередь только что была не пуста.
6381
+ queueEmptyAt: null
6382
+ },
5855
6383
  runtime.stateFilePath
5856
6384
  );
5857
6385
  const duplicateNote = result.data.duplicate ? " (\u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u043C\u0435\u0442\u0438\u043B \u044D\u0442\u043E \u043A\u0430\u043A \u043F\u043E\u0432\u0442\u043E\u0440 \u0443\u0436\u0435 \u043F\u0440\u0438\u043D\u044F\u0442\u043E\u0439 \u0441\u0434\u0430\u0447\u0438 \u2014 \u0437\u0430\u0434\u0432\u0430\u0438\u0432\u0430\u043D\u0438\u044F \u043D\u0435\u0442)" : "";
@@ -6010,7 +6538,7 @@ async function settleApproval(ack, commandCwd, runtime, waitSecUsed) {
6010
6538
  }
6011
6539
 
6012
6540
  // src/mcp/set-mode.ts
6013
- import { z as z37 } from "zod";
6541
+ import { z as z38 } from "zod";
6014
6542
 
6015
6543
  // src/cli/mode.ts
6016
6544
  function parseModeArg(raw) {
@@ -6051,7 +6579,7 @@ async function requestModeChange(modeArg, options = {}) {
6051
6579
 
6052
6580
  // src/mcp/set-mode.ts
6053
6581
  var setModeInputShape = {
6054
- mode: z37.enum(["auto", "manual"]).describe("auto \u2014 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u043D\u043E\u043C\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u0430\u0433\u0435\u043D\u0442\u0430, manual \u2014 \u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C (\xA710.2 \u0422\u0417)")
6582
+ mode: z38.enum(["auto", "manual"]).describe("auto \u2014 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u043D\u043E\u043C\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u0430\u0433\u0435\u043D\u0442\u0430, manual \u2014 \u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C (\xA710.2 \u0422\u0417)")
6055
6583
  };
6056
6584
  async function setModeHandler(args, runtime) {
6057
6585
  const result = await requestModeChange(args.mode, {
@@ -6066,20 +6594,20 @@ async function setModeHandler(args, runtime) {
6066
6594
  }
6067
6595
 
6068
6596
  // src/mcp/submit-work.ts
6069
- import { z as z38 } from "zod";
6597
+ import { z as z39 } from "zod";
6070
6598
  var submitWorkInputShape = {
6071
- branchName: z38.string().min(1).describe(
6599
+ branchName: z39.string().min(1).describe(
6072
6600
  "\u0418\u043C\u044F \u0432\u0435\u0442\u043A\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0431\u0435\u0440\u0438 \u0440\u043E\u0432\u043D\u043E \u0438\u0437 project.branchName \u0432 TaskPayload, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0432\u0435\u0440\u043D\u0443\u043B next_task. \u041D\u0435 \u043F\u0440\u0438\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u0441\u0432\u043E\u0451."
6073
6601
  ),
6074
- defaultBranch: z38.string().min(1).describe("\u041E\u0441\u043D\u043E\u0432\u043D\u0430\u044F \u0432\u0435\u0442\u043A\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u2014 \u0438\u0437 project.repos[].defaultBranch."),
6075
- commitPrefix: z38.string().min(1).describe('\u041F\u0440\u0435\u0444\u0438\u043A\u0441 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u2014 \u0438\u0437 project.commitPrefix, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "ATL-42:".'),
6076
- taskKey: z38.string().min(1).describe("\u041A\u043B\u044E\u0447 \u0437\u0430\u0434\u0430\u0447\u0438, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 ATL-42 \u2014 \u0438\u0437 task.key."),
6077
- taskTitle: z38.string().min(1).describe("\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0438\u0437 task.title."),
6078
- repoRoot: z38.string().min(1).optional().describe("\u041A\u043E\u0440\u0435\u043D\u044C git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u2014 \u0440\u0430\u0431\u043E\u0447\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043F\u0440\u043E\u0435\u043A\u0442\u0430."),
6079
- testsPassed: z38.boolean().optional().describe(
6602
+ defaultBranch: z39.string().min(1).describe("\u041E\u0441\u043D\u043E\u0432\u043D\u0430\u044F \u0432\u0435\u0442\u043A\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u2014 \u0438\u0437 project.repos[].defaultBranch."),
6603
+ commitPrefix: z39.string().min(1).describe('\u041F\u0440\u0435\u0444\u0438\u043A\u0441 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u2014 \u0438\u0437 project.commitPrefix, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "ATL-42:".'),
6604
+ taskKey: z39.string().min(1).describe("\u041A\u043B\u044E\u0447 \u0437\u0430\u0434\u0430\u0447\u0438, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 ATL-42 \u2014 \u0438\u0437 task.key."),
6605
+ taskTitle: z39.string().min(1).describe("\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0438\u0437 task.title."),
6606
+ repoRoot: z39.string().min(1).optional().describe("\u041A\u043E\u0440\u0435\u043D\u044C git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F. \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u2014 \u0440\u0430\u0431\u043E\u0447\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043F\u0440\u043E\u0435\u043A\u0442\u0430."),
6607
+ testsPassed: z39.boolean().optional().describe(
6080
6608
  "\u041F\u0440\u043E\u0448\u043B\u0438 \u043B\u0438 \u0442\u0435\u0441\u0442\u044B. \u0415\u0441\u043B\u0438 \u043F\u0440\u043E\u0435\u043A\u0442 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0437\u0435\u043B\u0451\u043D\u044B\u0445 \u0442\u0435\u0441\u0442\u043E\u0432, \u0430 \u0437\u0434\u0435\u0441\u044C false \u2014 \u0441\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442\u0441\u044F, \u0438 \u044D\u0442\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E."
6081
6609
  ),
6082
- requireTestsPassed: z38.boolean().optional().describe("ProjectSettings.requireTests \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0438\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0437\u0430\u0434\u0430\u0447\u0438.")
6610
+ requireTestsPassed: z39.boolean().optional().describe("ProjectSettings.requireTests \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u0438\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430 \u0437\u0430\u0434\u0430\u0447\u0438.")
6083
6611
  };
6084
6612
  async function submitWorkHandler(args, runtime) {
6085
6613
  const state = readState(runtime.stateFilePath);
@@ -6171,6 +6699,9 @@ async function submitViaPullRequest(args, runtime, currentTask) {
6171
6699
 
6172
6700
  // src/mcp/tasks.ts
6173
6701
  var createTaskInputShape = {
6702
+ dependsOn: taskCreatePayloadSchema.shape.dependsOn.describe(
6703
+ "ID \u0437\u0430\u0434\u0430\u0447, \u043E\u0442 \u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u043D\u043E\u0432\u0430\u044F \u0437\u0430\u0432\u0438\u0441\u0438\u0442: \u0435\u0451 \u043D\u0435 \u0432\u044B\u0434\u0430\u0434\u0443\u0442, \u043F\u043E\u043A\u0430 \u043E\u043D\u0438 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0442\u044B. \u0421\u0432\u044F\u0437\u0438 \u0441\u0442\u0430\u0432\u044F\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443 \u2014 \u0432\u0442\u043E\u0440\u043E\u0439 \u043F\u0440\u043E\u0445\u043E\u0434 \u043F\u043E \u0441\u043F\u0438\u0441\u043A\u0443 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D. \u041D\u0435\u043F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0430\u044F \u0441\u0441\u044B\u043B\u043A\u0430 (\u0447\u0443\u0436\u043E\u0439 \u043F\u0440\u043E\u0435\u043A\u0442, \u043D\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430) \u043C\u043E\u043B\u0447\u0430 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F: \u0437\u0430\u0434\u0430\u0447\u0430 \u0443\u0436\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430, \u0438 \u0442\u0435\u0440\u044F\u0442\u044C \u0435\u0451 \u0438\u0437-\u0437\u0430 \u043E\u0434\u043D\u043E\u0439 \u0441\u0432\u044F\u0437\u0438 \u043D\u0435\u043B\u044C\u0437\u044F."
6704
+ ),
6174
6705
  title: taskCreatePayloadSchema.shape.title.describe("\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A \u043D\u043E\u0432\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438, \u043A\u043E\u0440\u043E\u0442\u043A\u043E."),
6175
6706
  descriptionMd: taskCreatePayloadSchema.shape.descriptionMd.describe(
6176
6707
  "Markdown-\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u043E\u0432\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u0438 \u0437\u0430\u0447\u0435\u043C."
@@ -6209,7 +6740,8 @@ async function createTaskHandler(args, runtime) {
6209
6740
  executor: args.executor,
6210
6741
  blocksCurrent: args.blocksCurrent,
6211
6742
  ...args.acceptance ? { acceptance: args.acceptance } : {},
6212
- ...args.labels && args.labels.length > 0 ? { labels: args.labels } : {}
6743
+ ...args.labels && args.labels.length > 0 ? { labels: args.labels } : {},
6744
+ ...args.dependsOn && args.dependsOn.length > 0 ? { dependsOn: args.dependsOn } : {}
6213
6745
  });
6214
6746
  if (!parsedArgs.ok) return errorResult(parsedArgs.message);
6215
6747
  const state = readState(runtime.stateFilePath);
@@ -6235,8 +6767,15 @@ async function createTaskHandler(args, runtime) {
6235
6767
  const blockNote = result.data.currentTaskStatus ? `
6236
6768
 
6237
6769
  \u0422\u0432\u043E\u044F \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 ${state.currentTask?.taskKey ?? state.currentTask?.taskId ?? ""} \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D\u0430: \u043D\u043E\u0432\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441 \u2014 ${result.data.currentTaskStatus}. \u041F\u0440\u0435\u043A\u0440\u0430\u0442\u0438 \u0430\u043A\u0442\u0438\u0432\u043D\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430\u0434 \u043D\u0435\u0439 \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441; \u043A\u043E\u0433\u0434\u0430 \u0434\u043E\u0439\u0434\u0451\u0448\u044C \u0434\u043E \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F, \u043A\u043E\u0442\u043E\u0440\u043E\u0435 \u0441\u0442\u043E\u0438\u0442 \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C, \u0441\u0434\u0430\u0439 report(partial) \u0441 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435\u043C \u0441\u0434\u0435\u043B\u0430\u043D\u043D\u043E\u0433\u043E \u0438 \u0443\u043A\u0430\u0436\u0438 \u0432 nextSteps, \u0447\u0442\u043E \u0436\u0434\u0451\u0448\u044C \u043D\u043E\u0432\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443 ${result.data.task.key}.` : "";
6770
+ const requested = args.dependsOn?.length ?? 0;
6771
+ const linked = result.data.dependenciesLinked ?? 0;
6772
+ const depsNote = requested === 0 ? "" : linked === requested ? `
6773
+
6774
+ \u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E: ${linked}.` : `
6775
+
6776
+ \u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E ${linked} \u0438\u0437 ${requested}: \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0435 \u043F\u043E\u0434\u043E\u0448\u043B\u0438 (\u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u043B\u0438 \u043E\u043D\u0430 \u0438\u0437 \u0434\u0440\u0443\u0433\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430). \u041F\u0440\u043E\u0432\u0435\u0440\u044C \u0438\u0445 \u0438 \u043F\u0440\u0438 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u0441\u0432\u044F\u0436\u0438 \u0447\u0435\u0440\u0435\u0437 link_tasks.`;
6238
6777
  return textResult(
6239
- `\u0421\u043E\u0437\u0434\u0430\u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 ${result.data.task.key} (${result.data.task.status}) \u0434\u043B\u044F ${addressee}.${blockNote}`
6778
+ `\u0421\u043E\u0437\u0434\u0430\u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 ${result.data.task.key} (${result.data.task.status}) \u0434\u043B\u044F ${addressee}.${depsNote}${blockNote}`
6240
6779
  );
6241
6780
  }
6242
6781
  var listTasksInputShape = {
@@ -6415,6 +6954,33 @@ function registerTools(server, runtime) {
6415
6954
  },
6416
6955
  (args) => updateTaskHandler(args, runtime)
6417
6956
  );
6957
+ server.registerTool(
6958
+ "link_tasks",
6959
+ {
6960
+ title: "\u0421\u0432\u044F\u0437\u0430\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0438",
6961
+ description: "\u0417\u0430\u0434\u0430\u0442\u044C \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \xABA \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 B\xBB: \u043F\u043E\u043A\u0430 A \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0434\u0438\u0441\u043F\u0435\u0442\u0447\u0435\u0440 \u043D\u0435 \u0432\u044B\u0434\u0430\u0441\u0442 B. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0441 \u041B\u042E\u0411\u042B\u041C\u0418 \u0437\u0430\u0434\u0430\u0447\u0430\u043C\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430, \u0430\u0440\u0435\u043D\u0434\u0430 \u043D\u0435 \u043D\u0443\u0436\u043D\u0430 \u2014 \u0432 \u043E\u0442\u043B\u0438\u0447\u0438\u0435 \u043E\u0442 update_task. \u0422\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u043E\u043B\u044C \xAB\u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440\xBB \u043B\u0438\u0431\u043E \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430/\u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u0438. \u0421\u0432\u044F\u0437\u044C, \u0437\u0430\u043C\u044B\u043A\u0430\u044E\u0449\u0430\u044F \u043A\u043E\u043B\u044C\u0446\u043E, \u0431\u0443\u0434\u0435\u0442 \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442\u0430: \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u043D\u0451\u043C \u0436\u0434\u0430\u043B\u0438 \u0431\u044B \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0430 \u0432\u0435\u0447\u043D\u043E.",
6962
+ inputSchema: linkTasksInputShape
6963
+ },
6964
+ (args) => linkTasksHandler(args, runtime)
6965
+ );
6966
+ server.registerTool(
6967
+ "plan_task",
6968
+ {
6969
+ title: "\u0421\u043F\u043B\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443",
6970
+ description: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u043E\u043B\u044F \u041B\u042E\u0411\u041E\u0419 \u0437\u0430\u0434\u0430\u0447\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430: \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A, \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435, \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u043F\u0440\u0438\u0451\u043C\u043A\u0438, \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442, \u0442\u0438\u043F, \u0430\u0434\u0440\u0435\u0441\u0430\u0442\u0430, \u043C\u0438\u043B\u0441\u0442\u043E\u0443\u043D, \u043C\u0435\u0442\u043A\u0438, \u043E\u0446\u0435\u043D\u043A\u0443. \u0410\u0440\u0435\u043D\u0434\u0430 \u043D\u0435 \u043D\u0443\u0436\u043D\u0430 \u2014 \u0432 \u043E\u0442\u043B\u0438\u0447\u0438\u0435 \u043E\u0442 update_task. \u0422\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u043E\u043B\u044C \xAB\u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440\xBB \u043B\u0438\u0431\u043E \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430/\u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u0438. \u0421\u0422\u0410\u0422\u0423\u0421 \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F: \u0432 \u0440\u0430\u0431\u043E\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0443 \u043F\u0435\u0440\u0435\u0432\u043E\u0434\u0438\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A. \u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u0440\u0430\u0431\u043E\u0442\u044B \u0442\u043E\u0436\u0435 \u2014 \u043E\u043D\u0438 \u043F\u0440\u0438\u0445\u043E\u0434\u044F\u0442 \u0438\u0437 report.",
6971
+ inputSchema: planTaskInputShape
6972
+ },
6973
+ (args) => planTaskHandler(args, runtime)
6974
+ );
6975
+ server.registerTool(
6976
+ "unlink_tasks",
6977
+ {
6978
+ title: "\u0421\u043D\u044F\u0442\u044C \u0441\u0432\u044F\u0437\u044C \u043C\u0435\u0436\u0434\u0443 \u0437\u0430\u0434\u0430\u0447\u0430\u043C\u0438",
6979
+ description: "\u0423\u0431\u0440\u0430\u0442\u044C \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \xABA \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 B\xBB. \u0415\u0441\u043B\u0438 \u0441\u0432\u044F\u0437\u0438 \u043D\u0435 \u0431\u044B\u043B\u043E \u2014 \u044D\u0442\u043E \u043D\u0435 \u043E\u0448\u0438\u0431\u043A\u0430, \u0430 \u043E\u0442\u0447\u0451\u0442.",
6980
+ inputSchema: unlinkTasksInputShape
6981
+ },
6982
+ (args) => unlinkTasksHandler(args, runtime)
6983
+ );
6418
6984
  server.registerTool(
6419
6985
  "ask_human",
6420
6986
  {
@@ -6553,13 +7119,13 @@ function formatConnectResult(result) {
6553
7119
  }
6554
7120
 
6555
7121
  // src/hooks/pre-compact.ts
6556
- import { z as z39 } from "zod";
6557
- var PreCompactInputSchema = z39.object({
6558
- hook_event_name: z39.literal("PreCompact"),
6559
- session_id: z39.string(),
6560
- cwd: z39.string(),
6561
- trigger: z39.enum(["manual", "auto"]),
6562
- custom_instructions: z39.string().nullable()
7122
+ import { z as z40 } from "zod";
7123
+ var PreCompactInputSchema = z40.object({
7124
+ hook_event_name: z40.literal("PreCompact"),
7125
+ session_id: z40.string(),
7126
+ cwd: z40.string(),
7127
+ trigger: z40.enum(["manual", "auto"]),
7128
+ custom_instructions: z40.string().nullable()
6563
7129
  }).passthrough();
6564
7130
  function preCompactHook(_input, state, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
6565
7131
  const lastPreCompact = state.currentTask ? { ...state.currentTask, savedAt: now() } : null;
@@ -6568,14 +7134,14 @@ function preCompactHook(_input, state, now = () => (/* @__PURE__ */ new Date()).
6568
7134
 
6569
7135
  // src/hooks/pre-tool-use.ts
6570
7136
  import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
6571
- import { z as z40 } from "zod";
6572
- var PreToolUseInputSchema = z40.object({
6573
- hook_event_name: z40.literal("PreToolUse"),
6574
- session_id: z40.string(),
6575
- cwd: z40.string(),
6576
- tool_name: z40.string(),
6577
- tool_input: z40.record(z40.string(), z40.unknown()),
6578
- tool_use_id: z40.string()
7137
+ import { z as z41 } from "zod";
7138
+ var PreToolUseInputSchema = z41.object({
7139
+ hook_event_name: z41.literal("PreToolUse"),
7140
+ session_id: z41.string(),
7141
+ cwd: z41.string(),
7142
+ tool_name: z41.string(),
7143
+ tool_input: z41.record(z41.string(), z41.unknown()),
7144
+ tool_use_id: z41.string()
6579
7145
  }).passthrough();
6580
7146
  var NO_TRUST_CACHE_REASON = "\u041A\u044D\u0448 \u043F\u0440\u0430\u0432\u0438\u043B \u0434\u043E\u0432\u0435\u0440\u0438\u044F (TrustRules) \u043F\u0443\u0441\u0442 \u2014 \u043A\u043B\u0438\u0435\u043D\u0442 \u043D\u0438 \u0440\u0430\u0437\u0443 \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0430\u043B \u0438\u0445 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043D\u0435\u0442 agent:hello/trust:updated). \u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 \u043F\u0440\u043E\u0444\u0438\u043B\u044C (full/guarded/approve-all) \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u043D\u0435\u043B\u044C\u0437\u044F, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043F\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (fail-closed \u043F\u0440\u0438 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \xA710.4 \u0422\u0417) \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435: `quiel connect` (\u044D\u0442\u0430 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0430 \u0438\u043C\u0435\u043D\u043D\u043E \u0434\u043B\u044F \u0442\u0430\u043A\u043E\u0433\u043E \u0441\u043B\u0443\u0447\u0430\u044F, \u0441\u043C. isBootstrapCommand) \u2014 \u043F\u043E\u0441\u043B\u0435 \u0440\u0443\u043A\u043E\u043F\u043E\u0436\u0430\u0442\u0438\u044F \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043F\u0440\u0438\u0434\u0443\u0442, \u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u043D\u0430\u0447\u043D\u0443\u0442 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442\u044C. \u0415\u0441\u043B\u0438 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C, \u0432\u044B\u0437\u043E\u0432\u0438 request_approval.";
6581
7147
  var BOOTSTRAP_COMMAND_RE = /^quiel\s+(connect|status)\b/;
@@ -6807,13 +7373,13 @@ function readNewUsage(transcriptPath, offset) {
6807
7373
  }
6808
7374
 
6809
7375
  // src/hooks/session-start.ts
6810
- import { z as z41 } from "zod";
6811
- var SessionStartInputSchema = z41.object({
6812
- hook_event_name: z41.literal("SessionStart"),
6813
- session_id: z41.string(),
6814
- cwd: z41.string(),
6815
- source: z41.enum(["startup", "resume", "clear", "compact", "fork"]),
6816
- model: z41.string().optional()
7376
+ import { z as z42 } from "zod";
7377
+ var SessionStartInputSchema = z42.object({
7378
+ hook_event_name: z42.literal("SessionStart"),
7379
+ session_id: z42.string(),
7380
+ cwd: z42.string(),
7381
+ source: z42.enum(["startup", "resume", "clear", "compact", "fork"]),
7382
+ model: z42.string().optional()
6817
7383
  }).passthrough();
6818
7384
  var SESSION_RESET_SOURCES = /* @__PURE__ */ new Set([
6819
7385
  "startup",
@@ -6852,13 +7418,26 @@ function sessionStartHook(input, state) {
6852
7418
  }
6853
7419
 
6854
7420
  // src/hooks/stop.ts
6855
- import { z as z42 } from "zod";
6856
- var StopInputSchema = z42.object({
6857
- hook_event_name: z42.literal("Stop"),
6858
- session_id: z42.string(),
6859
- cwd: z42.string(),
6860
- stop_hook_active: z42.boolean(),
6861
- last_assistant_message: z42.string().optional()
7421
+ import { z as z43 } from "zod";
7422
+ var StopInputSchema = z43.object({
7423
+ hook_event_name: z43.literal("Stop"),
7424
+ session_id: z43.string(),
7425
+ cwd: z43.string(),
7426
+ stop_hook_active: z43.boolean(),
7427
+ last_assistant_message: z43.string().optional(),
7428
+ /**
7429
+ * Незавершённая фоновая работа сессии.
7430
+ *
7431
+ * ЧИТАЕМ ТОЛЬКО ДЛИНУ, И ЭТО НАМЕРЕННО. Внутренняя форма элементов в
7432
+ * документации не описана, а строить решение на угаданных полях —
7433
+ * ровно то, что §22.8 ТЗ запрещает. Нам достаточно ответа на вопрос
7434
+ * «есть ли незакрытая фоновая работа»: он не зависит от того, что
7435
+ * внутри.
7436
+ *
7437
+ * В §10.4 ТЗ этих полей нет — они появились в Claude Code позже.
7438
+ */
7439
+ background_tasks: z43.array(z43.unknown()).optional(),
7440
+ session_crons: z43.array(z43.unknown()).optional()
6862
7441
  }).passthrough();
6863
7442
  function taskLabel(state) {
6864
7443
  if (!state.currentTask) return "";
@@ -6871,6 +7450,13 @@ function stopHook(input, state) {
6871
7450
  if (state.mode !== AgentMode.AUTONOMOUS) {
6872
7451
  return { output: {} };
6873
7452
  }
7453
+ if (hasUnfinishedBackgroundWork(input)) {
7454
+ return {
7455
+ output: {
7456
+ systemMessage: "\u0410\u0433\u0435\u043D\u0442 \u0436\u0434\u0451\u0442 \u0444\u043E\u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u2014 \u0445\u0443\u043A \u043D\u0435 \u043C\u0435\u0448\u0430\u0435\u0442. \u0425\u043E\u0434 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u0441\u044F, \u043A\u043E\u0433\u0434\u0430 \u043E\u043D\u0430 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u0442\u0441\u044F."
7457
+ }
7458
+ };
7459
+ }
6874
7460
  if (state.currentTask) {
6875
7461
  return {
6876
7462
  output: {
@@ -6879,77 +7465,44 @@ function stopHook(input, state) {
6879
7465
  }
6880
7466
  };
6881
7467
  }
6882
- if (state.tasksCompletedInSession < state.contextResetAfterTasks) {
7468
+ if (state.tasksCompletedInSession >= state.contextResetAfterTasks) {
7469
+ return limitReached(state);
7470
+ }
7471
+ if (state.queueEmptyAt) {
6883
7472
  return {
6884
7473
  output: {
6885
- decision: "block",
6886
- reason: "\u0412\u044B\u0437\u043E\u0432\u0438 next_task \u0434\u043B\u044F \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438."
7474
+ systemMessage: "\u041E\u0447\u0435\u0440\u0435\u0434\u044C \u043F\u0443\u0441\u0442\u0430 \u2014 \u0430\u0433\u0435\u043D\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D. \u041F\u043E\u0437\u043E\u0432\u0438\u0442\u0435 \u0435\u0433\u043E \u0441\u043D\u043E\u0432\u0430, \u043A\u043E\u0433\u0434\u0430 \u043F\u043E\u044F\u0432\u044F\u0442\u0441\u044F \u0437\u0430\u0434\u0430\u0447\u0438: \u043E\u043D \u0432\u043E\u0437\u044C\u043C\u0451\u0442 \u0438\u0445 \u0441 \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u0436\u0435 next_task."
6887
7475
  }
6888
7476
  };
6889
7477
  }
7478
+ return {
7479
+ output: {
7480
+ decision: "block",
7481
+ reason: "\u0412\u044B\u0437\u043E\u0432\u0438 next_task \u0434\u043B\u044F \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438."
7482
+ }
7483
+ };
7484
+ }
7485
+ function hasUnfinishedBackgroundWork(input) {
7486
+ return (input.background_tasks?.length ?? 0) > 0 || (input.session_crons?.length ?? 0) > 0;
7487
+ }
7488
+ function limitReached(state) {
6890
7489
  const serverActions = [
6891
7490
  {
6892
7491
  type: "session-limit-reached",
6893
7492
  payload: { tasksCompletedInSession: state.tasksCompletedInSession }
6894
7493
  }
6895
7494
  ];
7495
+ const tasks = state.completedTaskIds.length;
7496
+ const submissions = state.tasksCompletedInSession;
7497
+ const counted = tasks > 0 && tasks !== submissions ? `${tasks} \u0437\u0430\u0434\u0430\u0447(\u0438) \u0437\u0430 ${submissions} \u0441\u0434\u0430\u0447(\u0438)` : `${submissions} \u0437\u0430\u0434\u0430\u0447(\u0438)`;
6896
7498
  return {
6897
7499
  output: {
6898
- systemMessage: `\u0421\u0435\u0441\u0441\u0438\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u043B\u0430 ${state.tasksCompletedInSession} \u0437\u0430\u0434\u0430\u0447(\u0438). \u0421\u0434\u0435\u043B\u0430\u0439\u0442\u0435 /clear \u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0440\u0435\u0436\u0438\u043C \u0430\u0433\u0435\u043D\u0442\u0430 \u0441\u043D\u043E\u0432\u0430.`
7500
+ systemMessage: `\u0421\u0435\u0441\u0441\u0438\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u043B\u0430 ${counted}. \u0421\u0434\u0435\u043B\u0430\u0439\u0442\u0435 /clear \u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0440\u0435\u0436\u0438\u043C \u0430\u0433\u0435\u043D\u0442\u0430 \u0441\u043D\u043E\u0432\u0430.`
6899
7501
  },
6900
7502
  serverActions
6901
7503
  };
6902
7504
  }
6903
7505
 
6904
- // src/hooks/stop-failure.ts
6905
- import { z as z43 } from "zod";
6906
- var StopFailureErrorTypeSchema = z43.enum([
6907
- "rate_limit",
6908
- "overloaded",
6909
- "authentication_failed",
6910
- "oauth_org_not_allowed",
6911
- "account_on_hold",
6912
- "billing_error",
6913
- "invalid_request",
6914
- "model_not_found",
6915
- "server_error",
6916
- "max_output_tokens",
6917
- "unknown"
6918
- ]);
6919
- var StopFailureInputSchema = z43.object({
6920
- hook_event_name: z43.literal("StopFailure"),
6921
- session_id: z43.string(),
6922
- cwd: z43.string(),
6923
- error: StopFailureErrorTypeSchema,
6924
- error_details: z43.unknown().optional(),
6925
- last_assistant_message: z43.string().optional()
6926
- }).passthrough();
6927
- function extractRetryAfterSec(errorDetails) {
6928
- if (typeof errorDetails === "number" && Number.isFinite(errorDetails) && errorDetails >= 0) {
6929
- return errorDetails;
6930
- }
6931
- if (errorDetails && typeof errorDetails === "object") {
6932
- const record = errorDetails;
6933
- for (const key of ["retryAfterSec", "retryAfter", "retry_after", "retry_after_seconds"]) {
6934
- const value = record[key];
6935
- if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
6936
- }
6937
- }
6938
- if (typeof errorDetails === "string") {
6939
- const match = errorDetails.match(/retry[-_ ]?after[^0-9]{0,10}(\d+)/i);
6940
- if (match?.[1] !== void 0) return Number(match[1]);
6941
- }
6942
- return void 0;
6943
- }
6944
- function stopFailureHook(input, _state) {
6945
- const retryAfterSec = extractRetryAfterSec(input.error_details);
6946
- const action = {
6947
- type: "agent:rate_limited",
6948
- payload: retryAfterSec === void 0 ? { error: input.error } : { error: input.error, retryAfterSec }
6949
- };
6950
- return { output: {}, serverActions: [action] };
6951
- }
6952
-
6953
7506
  // src/hooks/run.ts
6954
7507
  var HookInputSchema = z44.discriminatedUnion("hook_event_name", [
6955
7508
  SessionStartInputSchema,
@@ -7344,6 +7897,101 @@ function formatStatus(report) {
7344
7897
  return lines.join("\n");
7345
7898
  }
7346
7899
 
7900
+ // src/cli/wait-for-task.ts
7901
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, rmSync, writeFileSync as writeFileSync10 } from "fs";
7902
+ import { dirname as dirname6, join as join11 } from "path";
7903
+ var DEFAULT_POLL_INTERVAL_MS = 15e3;
7904
+ var DEFAULT_MAX_WAIT_MS = 8 * 60 * 60 * 1e3;
7905
+ var CONNECT_TIMEOUT_MS = 8e3;
7906
+ var PEEK_TIMEOUT_MS = 8e3;
7907
+ var WAIT_EXIT_IDLE = 0;
7908
+ var WAIT_EXIT_WAKE = 2;
7909
+ function skipReason(state) {
7910
+ if (state.mode !== AgentMode.AUTONOMOUS) {
7911
+ return "\u0440\u0435\u0436\u0438\u043C \u043D\u0435 \u0430\u0432\u0442\u043E\u043D\u043E\u043C\u043D\u044B\u0439";
7912
+ }
7913
+ if (state.currentTask) {
7914
+ return "\u0437\u0430\u0434\u0430\u0447\u0430 \u0443\u0436\u0435 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435";
7915
+ }
7916
+ return null;
7917
+ }
7918
+ function lockPath(stateFilePath) {
7919
+ return join11(dirname6(stateFilePath), "wait-for-task.lock");
7920
+ }
7921
+ function acquireLock(stateFilePath) {
7922
+ const path = lockPath(stateFilePath);
7923
+ if (existsSync11(path)) {
7924
+ const pid = Number.parseInt(readFileSync9(path, "utf8").trim(), 10);
7925
+ if (Number.isFinite(pid) && pid > 0 && isAlive(pid)) return false;
7926
+ }
7927
+ mkdirSync8(dirname6(path), { recursive: true });
7928
+ writeFileSync10(path, String(process.pid), "utf8");
7929
+ return true;
7930
+ }
7931
+ function releaseLock(stateFilePath) {
7932
+ rmSync(lockPath(stateFilePath), { force: true });
7933
+ }
7934
+ function isAlive(pid) {
7935
+ try {
7936
+ process.kill(pid, 0);
7937
+ return true;
7938
+ } catch {
7939
+ return false;
7940
+ }
7941
+ }
7942
+ function trimTrailingSlash4(url) {
7943
+ return url.endsWith("/") ? url.slice(0, -1) : url;
7944
+ }
7945
+ async function peekOnce(url, token, createSocket2) {
7946
+ const socket = createSocket2(`${trimTrailingSlash4(url)}/agent`, {
7947
+ auth: { token },
7948
+ autoConnect: false,
7949
+ reconnection: false
7950
+ });
7951
+ try {
7952
+ await connectOnce(socket, CONNECT_TIMEOUT_MS);
7953
+ const raw = await emitWithAckTimeout(socket, "task:peek", {}, PEEK_TIMEOUT_MS);
7954
+ const parsed = taskPeekAckSchema.safeParse(raw);
7955
+ return parsed.success ? parsed.data.available : null;
7956
+ } catch {
7957
+ return null;
7958
+ } finally {
7959
+ socket.disconnect();
7960
+ }
7961
+ }
7962
+ async function waitForTask(options = {}) {
7963
+ const context = resolveProjectContext(options.cwd ?? process.cwd(), options.credentialsFilePath);
7964
+ if (!context.ok) return { outcome: "idle", reason: "\u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D" };
7965
+ const stateFilePath = options.stateFilePath ?? getStateFilePath();
7966
+ const skip = skipReason(readState(stateFilePath));
7967
+ if (skip) return { outcome: "idle", reason: skip };
7968
+ if (!acquireLock(stateFilePath)) {
7969
+ return { outcome: "idle", reason: "\u043E\u0436\u0438\u0434\u0430\u0442\u0435\u043B\u044C \u0443\u0436\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D" };
7970
+ }
7971
+ const now = options.now ?? (() => Date.now());
7972
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
7973
+ const createSocket2 = options.createSocket ?? createSocket;
7974
+ const deadline = now() + (options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS);
7975
+ const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
7976
+ try {
7977
+ while (now() < deadline) {
7978
+ const reason = skipReason(readState(stateFilePath));
7979
+ if (reason) return { outcome: "idle", reason };
7980
+ const available = await peekOnce(context.config.url, context.credentials.token, createSocket2);
7981
+ if (available === true) {
7982
+ return {
7983
+ outcome: "wake",
7984
+ message: "\u0412 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043F\u043E\u044F\u0432\u0438\u043B\u0430\u0441\u044C \u0437\u0430\u0434\u0430\u0447\u0430. \u0412\u044B\u0437\u043E\u0432\u0438 next_task, \u0447\u0442\u043E\u0431\u044B \u0432\u0437\u044F\u0442\u044C \u0435\u0451."
7985
+ };
7986
+ }
7987
+ await sleep2(interval);
7988
+ }
7989
+ return { outcome: "idle", reason: "\u0438\u0441\u0442\u0435\u043A\u043B\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F" };
7990
+ } finally {
7991
+ releaseLock(stateFilePath);
7992
+ }
7993
+ }
7994
+
7347
7995
  // src/cli/program.ts
7348
7996
  var CLI_VERSION = VERSION;
7349
7997
  function printError(error) {
@@ -7426,6 +8074,16 @@ function createProgram() {
7426
8074
  printError(result.message);
7427
8075
  process.exitCode = 1;
7428
8076
  });
8077
+ program.command("wait-for-task").description("\u0416\u0434\u0430\u0442\u044C \u043F\u043E\u044F\u0432\u043B\u0435\u043D\u0438\u044F \u0437\u0430\u0434\u0430\u0447\u0438 \u0438 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E (\u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044F \u0445\u0443\u043A\u043E\u043C Stop \u0441 asyncRewake)").option("--cwd <dir>", "\u0420\u0430\u0431\u043E\u0447\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (\u0434\u043B\u044F \u0442\u0435\u0441\u0442\u043E\u0432/\u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438)").action(async (opts) => {
8078
+ const result = await waitForTask({ cwd: opts.cwd });
8079
+ if (result.outcome === "wake") {
8080
+ process.stderr.write(`${result.message}
8081
+ `);
8082
+ process.exitCode = WAIT_EXIT_WAKE;
8083
+ return;
8084
+ }
8085
+ process.exitCode = WAIT_EXIT_IDLE;
8086
+ });
7429
8087
  program.command("hook").description(
7430
8088
  "\u0412\u044B\u0437\u0432\u0430\u0442\u044C \u0445\u0443\u043A \u0430\u0433\u0435\u043D\u0442\u0441\u043A\u043E\u0439 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u044B (JSON \u043D\u0430 stdin) \u2014 \u043F\u0440\u043E\u043F\u0438\u0441\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043A\u043E\u043D\u0444\u0438\u0433\u043E\u043C \u043F\u0440\u0438 `quiel init`"
7431
8089
  ).argument("<name>", `\u0438\u043C\u044F \u0445\u0443\u043A\u0430: ${KNOWN_HOOK_NAMES.join(", ")}`).option(
@@ -7453,13 +8111,26 @@ function createProgram() {
7453
8111
  if (result.stderr.length > 0) process.stderr.write(`${result.stderr}
7454
8112
  `);
7455
8113
  process.exitCode = result.exitCode;
7456
- if (result.execution) sendHookActions(result.execution.serverActions);
8114
+ if (result.execution) {
8115
+ const rescued = await runRescueAction(result.execution.serverActions);
8116
+ const rescuedNote = describeRescue(rescued);
8117
+ if (rescuedNote) process.stderr.write(`${rescuedNote}
8118
+ `);
8119
+ sendHookActions(
8120
+ rescued.report ? [...result.execution.serverActions, rescued.report] : result.execution.serverActions
8121
+ );
8122
+ }
7457
8123
  return;
7458
8124
  }
7459
8125
  const execution = runCliHook(raw);
7460
8126
  process.stdout.write(`${execution.stdout}
7461
8127
  `);
7462
8128
  process.exitCode = execution.exitCode;
8129
+ const rescue = await runRescueAction(execution.serverActions);
8130
+ const note = describeRescue(rescue);
8131
+ if (note) process.stderr.write(`${note}
8132
+ `);
8133
+ if (rescue.report) sendHookActions([rescue.report]);
7463
8134
  sendHookActions(execution.serverActions);
7464
8135
  });
7465
8136
  program.command("mcp").description(