Жизненный цикл тикета
Статусы тикета
Заголовок раздела «Статусы тикета»| Статус | Строка в хранилище | Описание |
|---|---|---|
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 |