Перейти к содержимому

Тикеты и делегирование

Тикет — это единица работы, которую отслеживает Control Center. Он намеренно сделан примитивным артефактом: заголовок, тело, статус, владелец и место в дереве. Он не то, что исполняют агенты, — работа агента живёт в разговорах, а структурированный вывод запуска — в журнале запуска.

Назначение фиксирует владение; оно ничего не запускает

Заголовок раздела «Назначение фиксирует владение; оно ничего не запускает»

Это самое важное, что нужно усвоить про тикеты.

Установка assignedAgentId записывает поле и публикует доменное событие TicketAssigned. Это событие — сигнал для аудита и уведомлений и триггер конвейера. Назначение агенту не инициирует запуск. Диспетчера тикетов, который следил бы за assignedAgentId и будил этого агента, нет.

Исключение — команда: когда задан assignedTeamId, TeamRoutingService запускает лидера команды с рабочим протоколом и составом.

Если вы хотите, чтобы над тикетом поработал конкретный агент, попросите его в разговоре, либо это сделает шаг конвейера, либо его делегирует другой агент. Назначить тикет агенту и ждать — единственное, что никогда не даст работы.

Тикет может быть чисто локальным или опираться на внешний трекер, и то и другое он несёт в одной строке.

Зеркало — провайдер, внешний ключ, URL, заголовок, описание, приоритет, метки, сырой и нормализованный статус, отметки времени — это кэш, когда тикет удалённый. Источник истины там — удалённая система, и обновление переписывает зеркало целиком.

Оверлей — назначение, делегирование, привязка к пространству, родитель, проект, связанные PR — существует только в Control Center. Трекер о нём ничего не знает, и обновление его никогда не трогает.

Именно это разделение и делает синхронизацию безопасной: выгрузка из внешней системы может разрушительно обойтись с зеркалом, так и не потеряв метаданные оркестрации, надстроенные сверху.

Граф жизненного цикла обязателен для агентов, а не для вас

Заголовок раздела «Граф жизненного цикла обязателен для агентов, а не для вас»

Статусы идут так: backlog → open → inProgress → inReview → done, где blocked — боковой цикл, а failed / cancelled — остальные терминальные состояния.

Граф переходов применяется только на пути агента и автоматизации — инструментами MCP и реконсилерами. Недопустимый переход там логируется и игнорируется, поэтому запутавшийся агент не может провести тикет назад из терминального состояния.

Любое изменение в интерфейсе, сделанное пользователем, передаёт force: true и полностью обходит граф. Человек может перевести тикет в любой статус, который есть в селекторе, — в том числе открыть заново терминальный. Селектор не предлагает failed; агенты ставят его через fail_ticket. Граф — это ограждение для автоматизации, а не правило о том, как позволено двигаться работе.

TicketWorkflowService владеет каждой мутацией через единственную узловую точку: она загружает строку, проверяет, что та принадлежит рабочему пространству вызывающего, и записывает с ожидаемым version, чтобы два параллельных изменения не перезаписали друг друга молча.

Полную таблицу статусов, переходы, список полей и виды связей см. в разделе Жизненный цикл тикета.

У каждого тикета есть originKind. В перечислении пять значений — manual, pipelineStep, agentDelegation, externalSync, recovery, — но сегодня записываются только два: manual (значение по умолчанию в createTicket, в том числе для делегированных дочерних тикетов) и externalSync (движок синхронизации, при выгрузке из внешней системы). TicketWorkflowService.createTicket не принимает аргумент происхождения, поэтому pipelineStep, agentDelegation и recovery никогда не попадают в строку.

Делегирование — это когда агент создаёт работу для другого агента, и именно здесь могли бы утечь привилегии, поэтому защитные проверки живут на сервере в узловой точке, а не в инструкциях промпта.

delegate_task проходит через TicketWorkflowService.delegateGuarded, который вычисляет глубину и корень дочернего тикета по цепочке родителей, а затем выполняет все четыре проверки DelegationGuards: переход, который превысил бы предел глубины 3, переход назад к агенту, уже находящемуся в цепочке (обнаружение циклов), потолок автономии (действующий уровень делегата в пространстве не должен превышать уровень делегатора) и бюджетный конверт (делегирование списывается с оставшегося бюджета жёсткой остановки делегатора и не может выдать больше). Боевая конфигурация предоставляет оба порта-резолвера. Автономия и бюджет пропускаются, когда from_agent_id не указан (неограниченный бюджет или бюджет без политики тоже проходит). Отказ выбрасывается как DelegationRefusedException, сообщение которого доходит до делегирующего агента дословно, — громкий отказ, а не тихое бездействие.

delegate_ticket — старший брат, лишённый защитных проверок: он вызывает createTicket напрямую и пропускает все проверки. Предпочитайте delegate_task.

Не путайте предел глубины делегирования (3, для цепочек delegate_task) с пределом вложенности субагентов встроенного раннера (2). Это разные ограничения разных механизмов.

Отношения «родитель — подзадача» живут в tickets.parent_ticket_id и находятся в ведении setParent / clearParent. Это не строки ticket_links, поэтому всё, что читает связи тикета, должно объединять оба источника.

ticket_links хранит направленные рёбра между в остальном несвязанными тикетами:

Тип связи Значение
blocks Исходный тикет блокирует целевой
relatesTo Два тикета связаны (симметрично)
duplicateOf Исходный — дубликат целевого

Проект — это группировка тикетов в рамках рабочего пространства ради общей цели. Проекты существуют только в Control Center и никогда не синхронизируются с внешним провайдером.

Строки TicketCollaborator — это способ привязать к тикету больше одного субъекта с ролью assignee, collaborator или reviewer. Люди и агенты — равноправные строки: principalId плюс collaboratorType, без сигнального значения для «пользователя» и без внешнего ключа к любой из таблиц.

Тикет Control Center — первичен. Удаление тикета происходит только локально, поэтому тикет, опирающийся на внешнюю систему, при удалении не удаляется в трекере и может появиться снова при следующей синхронизации. Соисполнители и дочерние тикеты удаляются каскадом.

Учётные данные адаптера берутся из хранилищ учётных данных сервера (ключ Linear или токен GitHub, вставленный в разделе «Настройки», подхватывается при следующем запросе; окружение лишь заполняет эти хранилища начальными значениями).

Ни один узел конвейера не создаёт, не завершает и не отменяет тикеты — в палитре нет такого шага, и движок не вызывает TicketWorkflowService. Триггер конвейера всё же может сработать на TicketAssigned, TicketCompleted, TicketFailed или TicketCancelled (именно эти четыре события тикета реально отображает EventPayloadMapper). Столбца pipelineRunId у тикета нет; связь с конвейером живёт в журнале запуска агента.