# Work Item Provider Pipeline Design

## 목표

Plane Communication Foundation 다음 단계로 Plane/Jira 등 work item provider와 core task pipeline 사이의 최소 제품 흐름을 확정한다. 자동 실행 구현에 들어가기 전에 provider-neutral 생성, enqueue, 상태 투영, 중복 방지, 결과 발행 경계를 정리해 Workflow Core가 참조할 상태 변화와 실패 케이스를 명확히 한다.

## 단계

Work Item Provider Pipeline Design

## 상태

완료

## 구현 잠금

- 상태: 해제
- 결정 필요: 없음

## 범위

- Plane/Jira 같은 work item provider에서 core task를 생성하고 연결하는 entrypoint 후보 정리
- 수동 endpoint, webhook, polling 등 trigger 방식의 다음 구현 경로 결정
- core task enqueue 조건과 중복 생성 방지 기준 정리
- provider별 직접 접속부와 core 내부 pipeline 계약을 분리하는 adapter interface 정리
- provider board state와 agent 내부 실행 상태의 분리 계약 정리
- Plane/Jira에서 공통으로 읽히는 label/comment 기반 상태 투영 방식 정리
- task 완료/실패 결과를 provider comment/status로 발행하는 최소 정책 정리
- provider별 workspace/project/work item/state metadata 사용 방식 확정
- Workflow Core에 넘길 상태 변화, 실패 케이스, notification 요구사항 정리

## 필수 기능

- [x] [create-flow] Plane work item -> core task 생성/연결 흐름을 현재 구현 기준으로 문서화한다.
  - [x] [create-entrypoint] 초기 entrypoint는 기존 `POST /api/integrations/plane/tasks`를 기준선으로 삼는다.
  - [x] [create-scope] 생성 단계는 Plane work item 조회와 core task 저장까지만 담당한다.
  - [x] [create-pending] 생성된 task는 `pending` 상태로 남기고 enqueue는 별도 단계에서 처리한다.
  - [x] [create-metadata] Plane 연결 정보는 provider-neutral external ref와 work item metadata에 함께 저장한다.
- [x] [enqueue-trigger] 자동 enqueue 여부와 사용자/운영 트리거 경계를 결정한다.
  - [x] [user-todo] `backlog`에서 `todo`로 옮기는 주체는 사용자로 둔다.
  - [x] [todo-assignee] `todo` 상태여도 agent 작업자가 지정된 work item만 자동 실행 후보로 본다.
  - [x] [service-token] provider API 인증은 일반 사용자형 service account token을 기본으로 둔다.
  - [x] [token-local] Plane dev service account token은 ignored local file `.env.plane.local`에 저장하고, NomadCode project Admin 권한으로 검증한다.
  - [x] [assignee-agent] agent 작업자 지정 신호는 provider assignee가 `AGENT` 일반 사용자형 service account인 경우로 확정한다.
  - [x] [profile-metadata] worker profile과 실행 단계는 core metadata와 label/comment prefix로 표현한다.
  - [x] [trigger-webhook] webhook-first trigger와 polling fallback의 세부 조건을 확정한다.
  - [x] [labels-display] `worker:*`, `agent:*`, `phase:*` label은 실행 조건이 아니라 worker profile/실행 상태 표시용으로 둔다.
- [x] [adapter-interface] work item provider adapter interface를 설계한다.
  - [x] [dto-contract] core pipeline이 사용할 provider-neutral DTO를 정의한다.
  - [x] [provider-facets] work item 조회, comment 작성, status/state 변경, label projection 경계를 interface로 분리한다.
  - [x] [adapter-mapping] Plane adapter와 Jira adapter가 같은 interface를 구현할 수 있는지 검증한다.
  - [x] [provider-config] provider별 상태/라벨/comment 매핑은 adapter 설정으로 분리한다.
- [x] [generic-pipeline] 현재 Plane 고정 진입부를 provider-neutral 구조로 리팩토링한다.
  - [x] [route-service] `POST /api/integrations/plane/tasks`의 Plane 전용 흐름을 generic pipeline service 아래로 옮긴다.
  - [x] [handler-dto] HTTP handler가 Plane 타입과 직접 결합하지 않도록 요청 DTO와 변환 책임을 분리한다.
  - [x] [task-mapper] `buildPlaneCreateTaskInput`의 core task 생성 로직을 provider-neutral mapper로 분리한다.
  - [x] [compat-route] 기존 Plane endpoint는 compatibility entrypoint로 유지하고 generic endpoint/provider registry는 후속 별도 작업으로 분리한다.
- [x] [projection-contract] provider-neutral 상태와 projection 계약을 확정한다.
  - [x] [board-states] board state는 `backlog`, `todo`, `in_progress`, `testing`, `complete`, `cancel`로 둔다.
  - [x] [agent-metadata] `in_progress` 내부 agent 상태는 core task metadata를 canonical source로 둔다.
  - [x] [label-first] Plane/Jira provider projection은 label-first로 둔다.
  - [x] [desc-readonly] provider 본문(description)은 agent 실행 상태 저장소로 쓰지 않는다.
  - [x] [metadata-schema] core task metadata schema와 provider label mapping을 구현 대상으로 확정한다.
    - [x] [metadata-keys] canonical metadata key는 `agent_run_state`, `agent_phase`, `wait_type`, `status_reason`, `last_heartbeat_at`, `plan_ref`, `attempt`를 기본값으로 둔다.
    - [x] [metadata-storage] canonical metadata 저장 위치는 신규 `tasks.metadata jsonb`로 확정한다.
    - [x] [label-map] provider label mapping은 `agent:*`, `phase:*`, `worker:*` 공통 prefix를 우선 사용한다.
    - [x] [label-index] phase label은 작업별 phase index와 맞춰 `phase:0`부터 증가시키고, 최신 phase는 core metadata의 `agent_phase`를 canonical 값으로 둔다.
    - [x] [provider-delta] provider 공통 projection 계약을 먼저 맞추고 Plane/Jira 차이는 adapter별 capability와 mapping 설정으로 분리한다.
- [x] [result-policy] 완료/실패 결과의 provider comment/status update 정책을 정한다.
  - [x] [comment-prefix-sample] agent 단계 완료 기록은 prefix와 이모지가 있는 comment로 남기는 방향을 샘플 검증한다.
  - [x] [comment-timing] comment prefix 세트와 작성 타이밍을 확정한다.
- [x] [idempotency] 중복 생성 방지와 재시도 시 식별 기준을 정한다.
  - [x] [dedupe-key] provider-neutral idempotency key는 `provider`, `tenant`, `project`, `id` 조합으로 둔다.
  - [x] [active-dedupe] webhook, polling, 수동 entrypoint는 같은 active task가 있으면 새 task를 만들지 않는다.
  - [x] [retry-attempt] terminal task 이후 재시도는 명시적 retry intent와 `attempt` 증가로 구분한다.
- [x] [workflow-handoff] Workflow Core에서 처리할 lifecycle, retry, timeout, notification 요구사항을 정리한다.
  - [x] [lifecycle-owner] task lifecycle, retry, timeout은 Workflow Core 소유로 둔다.
  - [x] [projection-owner] provider comment/status/label 반영은 workflow 상태 변화의 projection으로 처리한다.
  - [x] [notification-scope] notification은 core 상태 변화 event 발행까지를 최소 범위로 두고 외부 협업 도구 발송은 후속 통합으로 넘긴다.

## 완료 기준

- [x] Plane/Jira work item에서 core task로 이어지는 provider-neutral pipeline entrypoint와 계약이 문서화되어 있다.
- [x] core workflow/pipeline 계층이 Plane package 타입에 직접 의존하지 않는다.
- [x] Plane 직접 접속부는 adapter 구현에 격리되고, Jira adapter 추가 지점이 명확하다.
- [x] enqueue 조건, idempotency 기준, 실패 표시 방식이 결정되어 있다.
- [x] provider board state, core canonical state, label/comment projection 계약이 문서화되어 있다.
- [x] completed/failed/cancelled 결과를 provider에 반영하는 최소 정책이 결정되어 있다.
- [x] Workflow Core가 pipeline 계약 질문 없이 상태 전이 구현을 시작할 수 있다.

## 범위 제외

- Plane webhook 구현
- Jira adapter 실제 구현
- provider registry와 generic work item endpoint 실제 구현
- Plane 전체 양방향 동기화
- Plane custom property 또는 work item type Pro 기능 의존
- task lifecycle, retry, timeout의 실제 구현
- 외부 협업 도구로 notification 발송
- Mattermost 메시지 발송 구현
- 복잡한 workflow DSL
- Plane token 생성/보관/교체 운영 절차

## 작업 컨텍스트

- 관련 경로: `services/core/internal/adapters/plane/`, `services/core/internal/http/`, `services/core/internal/storage/`, `services/core/internal/scheduler/`, `services/core/internal/workflow/`, `services/core/internal/workitem/`, `services/core/internal/workitempipeline/`, `services/core/README.md`
- 파일명 메모: 기존 Plane 중심 파일명 `plane-task-pipeline-design.md`는 대규모 rename을 피하기 위해 유지하되, 마일스톤 이름과 내용은 Work Item Provider Pipeline Design으로 일반화한다.
- 선행 작업: Plane Communication Foundation
- 후속 작업: Workflow Core
- 현재 코드 결합 상태:
  - HTTP router는 기존 compatibility route인 `POST /api/integrations/plane/tasks`를 유지한다.
  - handler는 Plane adapter package나 Plane DTO를 직접 import하지 않고, 로컬 `WorkItemReader` interface와 `workitem.Ref`, `workitem.WorkItem`으로 조회 결과를 다룬다.
  - `buildPlaneCreateTaskInput`은 HTTP package 안에서 core task payload/external ref를 직접 조립하지 않고 `workitem.BuildCreateTaskInput`에 위임한다.
  - `services/core/internal/workitem`은 provider-neutral DTO, optional facet interface, projection mapping, task create mapper를 제공한다.
  - Plane adapter는 `workitem.Provider`, `Reader`, `Commenter`, `StatusProjector`를 구현하며 `LabelProjector`는 capability false로 남긴다.
  - DB schema는 `external_provider`, `external_id`, `external_url`, `external_metadata`로 provider-neutral 토대를 유지하고, agent 실행 상태 snapshot은 `metadata jsonb`로 분리한다.
  - `services/core/internal/workitempipeline`은 provider-neutral reader, task mapper, `workflow.CreateTask` 호출을 묶는 generic pipeline service를 제공한다.
  - compatibility HTTP handler는 Plane request를 `workitem.Ref`로 변환한 뒤 `workitempipeline.Service`에 생성 흐름을 위임한다.
  - 이번 Milestone은 기존 `POST /api/integrations/plane/tasks` compatibility endpoint를 유지한 상태로 범위를 닫고, provider registry와 generic work item endpoint는 후속 별도 작업으로 분리한다.
- 초기 생성/연결 흐름:
  - 운영자 또는 상위 자동화가 `POST /api/integrations/plane/tasks`를 호출한다.
  - 요청 필수값은 `workspace_slug`, `project_id`, `work_item_id`이고, `state_id`, `external_url`, `comment`는 선택값으로 받는다.
  - core는 Plane adapter의 `workitem.Reader` 구현으로 work item detail을 조회해 provider-neutral title과 description 후보를 얻는다.
  - task title은 provider work item title을 우선하고, 비어 있으면 `work_item_id`를 사용한다.
  - task payload의 `message`는 요청 `comment`, provider text description, HTML description, title 순서로 선택하며 공백-only 후보는 건너뛴다.
  - task source는 `plane`, external provider는 `plane`, external id는 `work_item_id`로 저장한다.
  - provider-neutral metadata key `provider`, `tenant`, `project`, `id`, `state_id`, `external_url`은 `payload.work_item`과 `external_metadata`에 저장한다.
  - 생성 직후 task 상태는 `pending`이며, 이 entrypoint는 enqueue, Plane 상태 변경, 결과 comment 발행을 수행하지 않는다.
- 상태 설계 결정:
  - provider board state는 `backlog`, `todo`, `in_progress`, `testing`, `complete`, `cancel`의 소유권/검증 단계로 유지한다.
  - `in_progress`를 planning, implementing, review 같은 board state로 쪼개지 않고, agent 내부 실행 상태는 core task metadata에 canonical 값으로 저장한다.
  - agent 내부 실행 상태의 기본 canonical metadata key는 `agent_run_state`, `agent_phase`, `wait_type`, `status_reason`, `last_heartbeat_at`, `plan_ref`, `attempt`다.
  - agent가 작업 중 사용자 판단을 기다릴 때는 board state를 `in_progress`로 유지하고 `agent_run_state=waiting_for_user`와 `wait_type`으로 멈춤 이유를 표현한다.
  - `testing`은 agent가 plan, 구현, 자체 테스트, 자체 리뷰 루프를 끝낸 뒤 사용자가 직접 테스트하는 단계다.
  - provider projection은 라벨을 우선 사용한다. 예: `agent:waiting-user`, `agent:blocked`, `agent:failed`, `phase:0`, `phase:1`.
  - phase label은 `agent-task/archive`의 작업별 loop index처럼 0부터 증가시키며, provider label은 외부 표시/검색용이고 최신 phase의 canonical source는 core metadata의 `agent_phase`로 둔다.
  - provider 본문(description)은 작업 요구사항과 맥락의 원본으로 보고, agent 실행 상태를 매번 갱신하는 저장소로 쓰지 않는다.
  - Plane custom property는 현재 NomadCode dev project에서 `is_issue_type_enabled=False`라 기본 경로로 전제하지 않는다.
  - Plane 샘플 work item `NOMAD-13`은 `In Progress` state와 `agent:waiting-user`, `phase:planning` 라벨로 board state와 agent 내부 실행 상태 분리 방식을 보여준다. 신규 projection 계약에서는 phase label을 `phase:0`부터의 index형 label로 전환한다.
  - canonical metadata 저장 위치는 신규 `tasks.metadata jsonb`로 확정한다. `payload`는 task 생성 입력/요구사항, `external_metadata`는 provider 연결 정보, `metadata`는 agent 실행 상태의 최신 snapshot으로 분리한다.
  - `services/core/migrations/00003_add_task_metadata.sql`은 `tasks.metadata jsonb NOT NULL DEFAULT '{}'`를 추가한다.
  - workflow create/update 경계는 invalid metadata JSON을 거부하고 빈 값 또는 `null`은 `{}`로 정규화한다.
- trigger/auth 결정:
  - 사용자가 `backlog`에서 `todo`로 이동시키는 행위가 AI 작업 위임 의사로 간주될 수 있는 첫 관문이다.
  - core는 `todo` 상태와 agent 작업자 지정 신호가 함께 있을 때만 자동 실행 후보로 본다.
  - agent 작업자 지정 신호는 provider assignee가 `AGENT` display name을 가진 일반 사용자형 service account인 경우다.
  - 사용자가 `todo`로 작업을 옮기거나 `todo` 상태로 바로 생성하고, assignee를 `AGENT`로 지정하면 자동 실행 후보가 된다.
  - provider webhook을 우선 trigger로 사용한다.
  - polling은 webhook 누락, delivery 장애, core downtime 복구를 위한 fallback/reconciliation 경로로만 둔다.
  - webhook 수신 시 payload만 신뢰하지 않고 provider API로 work item을 다시 조회한 뒤 실행 조건을 재검증한다.
  - webhook과 polling 모두 동일한 idempotency 검사와 active task 중복 방지 로직을 통과해야 한다.
  - fallback polling 대상은 전체 provider item이 아니라 `todo` 상태와 agent 작업자 지정 신호가 있는 후보로 좁힌다.
  - provider API 호출과 댓글 작성은 일반 사용자형 service account token으로 수행한다.
  - Plane dev token은 repo root의 ignored `.env.plane.local`에서 로드하며, 해당 token 계정은 NomadCode project Admin 권한으로 API 조회/댓글 작성 smoke가 성공했다.
  - worker assignment identity는 `AGENT` assignee로 판단한다. 실제 worker profile과 agent 실행 단계는 core metadata, provider label, comment prefix에 남긴다.
  - Plane bot user는 UI 필터와 내장 agent trigger 경로가 있어 1차 작업자 모델로 전제하지 않는다.
- provider abstraction 결정:
  - Plane은 첫 구현 provider일 뿐이며 core pipeline의 도메인 모델이 되어서는 안 된다.
  - Jira도 같은 workflow state, label/comment projection, idempotency 계약을 공유할 수 있어야 한다.
  - provider-neutral adapter contract는 `workitem.Ref`, `WorkItem`, `CommentInput`, `StatusProjection`, `LabelProjection`, `Mapping`으로 둔다.
  - provider별 API 인증, URL, workspace/project/issue 식별자, status id, label id는 adapter 설정과 metadata로만 다룬다.
  - core의 canonical 상태와 agent 실행 metadata는 provider에서 읽어온 값이 아니라 NomadCode task 상태의 진실로 둔다.
  - provider별 차이는 공통 `workitem` projection DTO와 capability를 먼저 통과시킨 뒤, Plane/Jira adapter 내부 mapping과 optional facet 구현으로 분리한다.
  - public generic endpoint와 provider registry는 idempotency, result projection, webhook/polling 경계가 닫힌 뒤 별도 작업에서 설계/구현한다.
- comment 기록 규칙 후보:
  - `🧭 PLAN | <요약>`: plan 작성 또는 계획 검토 단계 완료 기록
  - `🛠️ WORK | <요약>`: 구현 또는 문서 반영 단계 완료 기록
  - `🔎 REVIEW | <요약>`: 코드리뷰/self-review 단계 완료 기록
  - `✅ VERIFY | <요약>`: 테스트/검증 완료 기록
  - 각 comment 본문은 1~2줄 요약을 기본으로 하며, 자세한 실행 로그나 상태 metadata는 core에 남긴다.
- result projection 정책:
  - provider comment는 사람이 볼 가치가 있는 전환점에만 남긴다. heartbeat, 내부 metadata update, polling reconciliation 같은 잦은 변화는 comment로 남기지 않는다.
  - comment prefix는 `PLAN`, `WORK`, `REVIEW`, `VERIFY`, `BLOCKED`, `FAILED`, `COMPLETE`, `CANCELLED`를 기본 세트로 둔다.
  - `PLAN`, `WORK`, `REVIEW`, `VERIFY` comment는 해당 단계가 성공적으로 끝났을 때 1회 남긴다.
  - `BLOCKED` comment는 `agent_run_state=waiting_for_user` 또는 provider/API 차단처럼 사람 조치가 필요한 상태로 전환될 때 남긴다.
  - `FAILED`, `COMPLETE`, `CANCELLED` comment는 terminal 상태가 확정될 때 남긴다.
  - provider board state update는 canonical task 상태를 투영한다. `completed`는 `complete`, `canceled`는 `cancel`로 보낸다.
  - `failed`는 work item 자체가 끝난 것이 아니므로 기본 board state를 `in_progress`에 두고 `agent:failed` label과 `FAILED` comment로 실패를 표시한다. 명시적 cancel은 `cancel` state로 보낸다.
  - provider comment 중복 방지는 `external_source=nomadcode`와 `external_id=<task-id>:<attempt>:<prefix>` 조합을 우선 사용한다.
- idempotency/retry 기준:
  - provider-neutral idempotency key는 `provider`, `tenant`, `project`, `id` 조합이다. 현재 Plane compatibility route에서는 `plane`, `workspace_slug`, `project_id`, `work_item_id`에 해당한다.
  - `pending`, `queued`, `running` task가 같은 idempotency key로 이미 있으면 webhook, polling, 수동 entrypoint 모두 새 task를 만들지 않고 기존 active task를 반환하거나 no-op으로 처리한다.
  - `completed` task는 기본적으로 자동 재생성하지 않는다. provider work item이 reopen되거나 사용자가 명시적 retry intent를 준 경우에만 새 attempt를 만든다.
  - `failed`, `canceled` task 이후 자동 재시도는 하지 않는다. retry는 명시적 intent가 있을 때 `metadata.attempt`를 증가시키고 같은 idempotency key에 새 attempt를 연결한다.
  - webhook과 polling은 provider payload만 신뢰하지 않고 provider API 재조회 후 실행 조건과 idempotency를 다시 확인한다.
- Workflow Core handoff:
  - work item pipeline은 provider lookup, task 생성 입력 조립, idempotency 판단, provider projection DTO 구성까지만 책임진다.
  - task lifecycle 전이는 Workflow Core가 소유한다. 기본 전이는 `pending -> queued -> running -> completed|failed|canceled`다.
  - retry, timeout, heartbeat, terminal 상태 확정은 Workflow Core가 소유하고, provider에는 그 결과만 projection한다.
  - IOP/A2A/model runtime은 외부 실행 표면이며, NomadCode Workflow Core는 외부 호출의 timeout, retry envelope, 결과 정규화, 상태 기록을 책임진다. 외부 런타임 내부 retry 정책은 IOP/A2A/model adapter의 책임으로 둔다.
  - notification의 최소 범위는 core 내부 상태 변화 event 발행이다. Mattermost 같은 외부 협업 도구 발송은 External Integration에서 별도로 구현한다.
  - provider projection 실패는 task terminal 상태를 되돌리지 않는다. projection 작업은 retry 가능한 부수 효과로 기록하고 재시도한다.
- 현재 지점 / 착수 상태:
  - 현재 상태는 `완료`이며, provider adapter interface 설계, Plane adapter facet 검증, provider-neutral task mapper, generic pipeline service 추출, HTTP compatibility endpoint의 Plane DTO 의존 제거가 완료됐다.
  - `agent-task/archive/2026/05/provider_neutral_plane_entrypoint/01_pipeline_mapper/complete.log`에서 `workitem.TaskCreateInput`과 `workitem.BuildCreateTaskInput` 추가 및 whitespace fallback 회귀 수정이 PASS로 정리됐다.
  - `agent-task/archive/2026/05/provider_neutral_plane_entrypoint/02+01_http_plane_entrypoint/complete.log`에서 HTTP handler의 provider-neutral reader/mapper 전환과 compatibility route 유지가 PASS로 정리됐다.
  - comment prefix 작성 타이밍, idempotency/retry 기준, Workflow Core handoff가 문서로 확정됐다.
  - 다음 진행 후보는 Workflow Core 구현 계획이다.
- 확인 필요: 없음
