티켓 수명주기
티켓 상태
섹션 제목: “티켓 상태”| 상태 | 저장 문자열 | 설명 |
|---|---|---|
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 도구와 조정기 경로에서 이를 검사합니다. 이 경로에서 허용되지 않는 전이는 로그에 기록되고 무시됩니다. UI에서 사용자가 일으키는 모든 상태 변경은 force: true를 전달해 그래프를 완전히 우회하므로, 사람은 선택기에 나열된 어떤 상태로든(failed를 제외한 모든 상태) 티켓을 옮길 수 있으며 종료된 상태를 다시 여는 것도 가능합니다. 에이전트는 fail_ticket으로 failed를 표시합니다.
상태는 언제나 자기 자신으로 전이될 수 있습니다. 종료 상태에는 나가는 간선이 없습니다.
| 시작 상태 | 허용되는 대상 |
|---|---|
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 |
원격이 소유하며 로컬에 미러링됨 |
동기화 어댑터는 Linear, GitHub Issues, Jira Cloud, ClickUp용으로
packages/cc_infra/lib/src/tickets/sync/ 아래에 있습니다. 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은 행 id가 아닌 별도의 UUID를 여기에 기록 |
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? |
배정된 주체의 id(메타데이터일 뿐이며, 배정해도 아무것도 디스패치되지 않음) |
assigneeType |
PrincipalType |
assignedAgentId가 가리키는 주체의 종류 — user 또는 agent(기본값 agent) |
createdByType |
PrincipalType? |
티켓을 생성한 주체의 종류(user 또는 agent)이며, 레거시/시스템 행에서는 null |
createdById |
String? |
생성한 주체의 id |
assignedTeamId |
String? |
배정된 팀(메타데이터일 뿐) |
delegatedByAgentId |
String? |
이 티켓을 위임한 에이전트 |
delegationDepth |
int |
위임 체인에서의 깊이이며 루트는 0이고, 최대 깊이 상한과 비교해 검사함 |
delegationRootTicketId |
String? |
위임 체인의 루트이며, 루트 티켓에서는 null |
spaceId |
String? |
티켓이 파생된 대화 또는 위임한 에이전트의 스페이스 |
errorMessage |
String? |
status가 failed일 때 설정됨 |
linkedPrIds |
List<String> |
이 티켓이 연결된 PR 노드 id |
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 — 따라서 로컬 티켓은 행 id가 아니라 생성 시 별도로 부여된 UUID를 표시합니다).
티켓 관계
섹션 제목: “티켓 관계”저장된 간선은 ticket_links의 방향성 행입니다:
| 유형 | 저장 문자열 | 의미 |
|---|---|---|
blocks |
blocks |
소스가 대상을 차단함 |
relatesTo |
relates_to |
대칭 관계 |
duplicateOf |
duplicate_of |
소스가 대상의 중복임 |
UI와 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를 호출합니다 |
| 워크스페이스 격리 | 로드한 행의 workspaceId가 호출자의 것과 일치하지 않으면 _assertWorkspace가 WorkspaceMismatchException을 던집니다 |
| 낙관적 동시성 | _mutate는 방금 읽은 version을 expectedVersion으로 설정해 기록하고, ConcurrencyConflictException이 발생하면 최신 상태를 다시 읽어 재시도합니다 |
| 배타적 점유 | tryCheckout / releaseCheckout이며, 이미 체크아웃된 티켓을 두 번째 에이전트가 차지하려 하면 CheckoutConflictException이 발생합니다 |
| 전이 가드 | canTransitionTo이며, force가 false일 때만 적용됩니다 |