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

Жизненный цикл тикета

Статус Строка в хранилище Описание
backlog backlog Зафиксирован, ещё не разобран в активную очередь
open open Разобран и готов к работе, но ещё не начат
inProgress inProgress Активно выполняется
blocked blocked Начат, но заблокирован чем-то внешним
inReview inReview Работа выполнена, ожидает проверки
done done Успешно завершён (терминальный)
failed failed Завершён с ошибкой (терминальный)
cancelled cancelled Отменён (терминальный)

Значения статусов хранятся в camelCase (inProgress, inReview), никогда не in_progress. Сохранённое значение null разбирается как open; неизвестное значение выбрасывает исключение, а не приводит тип.

tryParseLoose принимает псевдонимы, переданные агентом, игнорируя регистр и - / _ / пробел:

Статус Принимаемые псевдонимы
open todo, ready, pending, new
inProgress doing, started, active, wip
blocked stuck
inReview review, reviewing
done closed, complete, completed, finished
failed error, errored
cancelled canceled, abandoned, dropped
backlog ──→ open ──→ inProgress ──→ inReview ──→ done
│ │ │
│ └──→ failed ┘
└──→ blocked ──→ inProgress
Any non-terminal status ──→ cancelled; inReview ──→ failed

Этот граф обязателен только для агентов и автоматизации. TicketWorkflowService.transitionStatus проверяет его, когда force равно false, — это инструменты MCP и реконсилеры. Недопустимый переход там логируется и игнорируется. Любое изменение статуса пользователем в интерфейсе передаёт force: true и полностью обходит граф, поэтому человек может перевести тикет в любой статус из селектора (любой, кроме failed), в том числе открыть заново терминальный. Агенты помечают failed через fail_ticket.

Статус всегда может перейти в себя. У терминальных состояний нет исходящих рёбер.

Из Допустимые целевые статусы
backlog open, cancelled
open inProgress, blocked, cancelled
inProgress blocked, inReview, done, failed, cancelled
blocked inProgress, cancelled
inReview inProgress, done, failed, cancelled
done — (терминальный)
failed — (терминальный)
cancelled — (терминальный)

Хранится целым числом в родной для Linear шкале 0–4. Неизвестное значение разбирается как none.

Приоритет Хранится как
none 0
urgent 1
high 2
medium 3
low 4

Неизвестное или null-значение в хранилище разбирается как local.

Провайдер Состояние
local Полностью хранится в базе данных рабочего пространства; значение по умолчанию
linear Принадлежит удалённой системе, зеркалируется локально

Адаптеры синхронизации лежат в packages/cc_infra/lib/src/tickets/sync/ для Linear, GitHub Issues, Jira Cloud и ClickUp. GitHub Issues — только вендор адаптера синхронизации (vendorId: github) и не является значением TicketProvider, поэтому загруженная из GitHub задача хранится с originKind: externalSync и provider: local по умолчанию. Сопоставление вендоров живёт в ticket_sync_links.

createTicket записывает manual. Выгрузка из внешней системы записывает externalSync. Делегированные дочерние тикеты остаются manual.

Значение Смысл
manual Значение по умолчанию в createTicket (строки, созданные человеком, агентом и делегированием)
externalSync Устанавливает TicketSyncEngine при выгрузке из внешней системы

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

Поле Тип Описание
id String UUID v4
workspaceId String Рабочее пространство-владелец
provider TicketProvider Бэкенд — владелец канонических данных (по умолчанию local)
externalKey String? Родной для провайдера ключ (например, LIN-123). Локальный createTicket записывает сюда отдельный UUID, а не id строки
url String? Веб-URL в удалённом провайдере
title String Краткое описание; не может быть пустым
description String? Более длинное тело в Markdown
priority TicketPriority По умолчанию none
labels List<String> Метки произвольной формы
status TicketStatus Канонический нормализованный статус
rawStatus String? Родное для удалённого провайдера название состояния, хранимое для отображения без потерь
externalSyncedAt DateTime? Время последней мультивендорной синхронизации; null до первой. Сопоставление по вендорам живёт в ticket_sync_links
parentTicketId String? Родитель в дереве делегирования / декомпозиции
projectId String? Проект-владелец; никогда не отправляется в удалённую систему
assignedAgentId String? Идентификатор назначенного субъекта (только метаданные — назначение ничего не запускает)
assigneeType PrincipalType Какого рода субъект называет assignedAgentId — user или agent (по умолчанию agent)
createdByType PrincipalType? Род субъекта, создавшего тикет (user или agent); null для устаревших и системных строк
createdById String? Идентификатор создавшего субъекта
assignedTeamId String? Назначенная команда (только метаданные)
delegatedByAgentId String? Агент, делегировавший этот тикет
delegationDepth int Глубина в цепочке делегирования; у корня 0, проверяется на соответствие пределу максимальной глубины
delegationRootTicketId String? Корень цепочки делегирования; null для корневого тикета
spaceId String? Разговор, из которого выделен тикет, или пространство делегирующего агента
errorMessage String? Устанавливается, когда status равен failed
linkedPrIds List<String> Идентификаторы узлов PR, с которыми связан этот тикет
metadata Map<String, dynamic> Мешок произвольной формы
createdAt DateTime Время создания
startedAt DateTime? Когда началась работа
blockedAt DateTime? Когда тикет был заблокирован
cancelledAt DateTime? Когда тикет был отменён
completedAt DateTime? Когда тикет успешно завершился
finishedAt DateTime? Когда тикет достиг любого терминального состояния
updatedAt DateTime Последняя мутация (обновление зеркала или изменение оверлея)
version int Счётчик оптимистичного контроля параллелизма, увеличивается при каждой мутации
originKind TicketOriginKind Как был создан тикет (по умолчанию manual)
collaborators List<TicketCollaborator> Наполняется репозиторием по требованию

Производные: isTerminal, isRemote (provider != local), isAssignedToUser и displayKey (externalKey ?? id — поэтому локальные тикеты отображают отдельный UUID, созданный при создании, а не id строки).

Сохранённые рёбра — это направленные строки в ticket_links:

Тип Строка в хранилище Значение
blocks blocks Исходный блокирует целевой
relatesTo relates_to Симметричная связь
duplicateOf duplicate_of Исходный — дубликат целевого

Словарь, на котором говорят интерфейс и ticket_relation, — это TicketRelationKind, выводимый из сохранённого ребра и того, на каком конце находится субъект:

Вид Выводится из
blockedBy Строка blocks, где субъект — целевой
blocking Строка blocks, где субъект — исходный
relatedTo Строка relates_to (симметричная)
duplicateOf Строка duplicate_of, где субъект — исходный
duplicatedBy Строка duplicate_of, где субъект — целевой
subIssueOf tickets.parent_ticket_id у субъекта
parentOf tickets.parent_ticket_id у другого тикета

Связи «родитель — подзадача» — не строки ticket_links, они живут в tickets.parent_ticket_id.

Эти восемь классов живут в packages/cc_domain/lib/core/domain/events/ticketing_events.dart. Классов TicketStarted, TicketDelegated, TicketCollaboratorAdded и ExternalTicketWebhookReceived не существует.

Событие Публикуется, когда
TicketCreated Тикет создан
TicketAssigned Тикет назначен субъекту или команде
TicketCompleted Работа успешно завершается
TicketFailed Работа не удалась
TicketCancelled Тикет отменён
TicketStatusChanged Любое изменение статуса
TicketReassigned Тикет переназначен
TicketDetailsUpdated Меняются заголовок, описание, приоритет или метки

TicketAssigned — сигнал для аудита и уведомлений и триггер конвейера. Назначение агенту не инициирует запуск: диспетчера тикетов, который следил бы за assignedAgentId, нет. Исключение — назначение команде: TeamRoutingService запускает лидера команды. Добавление соисполнителя записывает строку и ничего не публикует. Вебхук вендора применяется через TicketSyncEngine без доменного события.

TicketWorkflowService владеет каждой мутацией.

Механизм Подробности
Единственная узловая точка Все мутации идут через _mutate, который загружает строку и вызывает _assertWorkspace перед применением
Изоляция рабочего пространства _assertWorkspace выбрасывает WorkspaceMismatchException, когда workspaceId загруженной строки не совпадает с идентификатором вызывающего
Оптимистичный контроль параллелизма _mutate записывает с expectedVersion, равным только что прочитанному version, и повторяет попытку при ConcurrencyConflictException, заново читая свежее состояние
Эксклюзивная блокировка tryCheckout / releaseCheckout; второй агент, претендующий на уже занятый тикет, получает CheckoutConflictException
Ограждение переходов canTransitionTo, применяется только когда force равно false