# Milestone: Plane Work Item Webhook Intake

## 위치

- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`

## 목표

Plane에서 상위 work item 생성, 상태 변경, assignee 변경 이벤트가 발생하면 NomadCode Core가 이를 webhook으로 받아 `Backlog + AGENT assignee` 조건을 재검증하고 기존 Plane-origin Milestone creation sync pipeline을 시작한다. 이 마일스톤이 끝나면 사용자는 Plane UI에서 티켓을 만들고 AGENT에 assign하는 동작만으로 NomadCode authoring flow가 시작되는지 확인할 수 있다.

## 상태

[완료]

## 구현 잠금

- 상태: 해제
- 결정 필요:
  - [x] dev Plane에서 호출할 NomadCode Core webhook URL은 `http://toki-labs.com:18010/api/integrations/plane/webhook`로 둔다. `plane.toki-labs.com`은 Plane의 동일 호스트/하위 도메인 차단 규칙에 걸리므로 사용하지 않는다.
  - [x] Plane webhook 인증 방식은 Plane UI에서 발급한 webhook secret/signature 검증으로 둔다. secret 원문은 tracked 파일에 기록하지 않고 SOPS encrypted file `secrets/nomadcode.dev.sops.yaml`의 `PLANE_WEBHOOK_SECRET`으로 보관한다.
  - [x] AGENT assignee로 사용할 Plane user id는 `5d116a77-d3df-4f54-80bf-eca61e0118c4`로 확인했다.
  - [x] webhook-only live smoke는 기존 Backlog item `NOMAD-10`, `NOMAD-11`의 AGENT assign 이벤트로 확인했다.

## 범위

- Plane native webhook payload 샘플을 확보하고 work item 생성, 상태 변경, assignee 변경 이벤트에서 필요한 필드를 식별한다.
- NomadCode Core에 Plane raw webhook receiver endpoint를 추가한다.
- Plane webhook secret/signature 또는 동등한 shared-secret 검증을 구현하고, raw secret은 tracked 파일과 로그에 남기지 않는다.
- webhook payload는 변경 힌트로만 사용하고, 최종 trigger 판정 전 Plane API로 work item을 다시 조회한다.
- 기존 `workitempipeline` trigger gate를 재사용해 `Backlog + AGENT assignee` 조건, actor/self guard, project sync setting, workspace provision, slot reservation 경계를 유지한다.
- 중복 webhook, 재시도, NomadCode self mutation event가 들어와도 중복 task나 중복 Milestone authoring run을 만들지 않도록 idempotency 기준을 고정한다.
- dev Plane project에 webhook을 등록하고, 사용자가 Plane UI에서 상위 티켓 생성/assign만으로 NomadCode flow가 시작되는 smoke를 확인한다.

## 기능

### Epic: [plane-webhook] Plane Webhook Intake

Plane에서 발생한 work item event를 NomadCode creation sync trigger로 안전하게 정규화한다.

- [x] [payload-sample] dev Plane webhook payload 샘플과 이벤트 종류를 확보하고 receiver 입력 fixture 또는 문서화된 contract candidate로 남긴다. 검증: raw secret/token 없이 work item create/update, state change, assignee change 후보 payload shape를 확인한다.
- [x] [webhook-receiver] `POST /api/integrations/plane/webhook` receiver를 추가하고 webhook auth 또는 shared-secret 검증을 통과한 요청만 처리한다. 검증: HTTP handler 테스트에서 인증 실패, payload 오류, 정상 accepted 응답을 확인한다.
- [x] [event-normalize] Plane webhook payload를 provider-neutral work item ref와 actor/change hint로 정규화하되, 최종 work item 상태는 Plane API 재조회 결과를 사용한다. 검증: payload 필드 누락 또는 stale event가 있어도 fetch 기반 trigger 판정으로 수렴한다.
- [x] [trigger-dispatch] `Backlog + AGENT assignee` 이벤트만 기존 `workitempipeline.CreateTaskFromWorkItem` 경로로 넘기고 나머지는 side effect 없이 ignored 처리한다. 검증: ignored event가 workspace provision 또는 slot reservation을 발생시키지 않는다.
- [x] [loop-idempotency] duplicate webhook, provider retry, NomadCode self actor event를 중복 creation task 없이 처리한다. 검증: 같은 provider/work item/revision 재처리와 self actor 입력이 task 중복 생성으로 이어지지 않는다.
- [x] [plane-registration] dev Plane project에 NomadCode webhook을 등록하고 운영자가 바꿔야 하는 URL, secret, event scope를 tracked 문서나 roadmap에 secret 없이 남긴다. 검증: Plane DB/UI에서 active project webhook이 존재하고 target이 NomadCode receiver를 가리키는지 확인한다.
- [x] [live-smoke] Plane UI에서 상위 티켓을 만들고 Backlog 상태에서 AGENT에 assign하면 NomadCode task가 생성되고 Milestone authoring flow가 시작된다. 검증: 테스트 Plane 티켓 id, Core task id, authoring run 시작 또는 ignored 사유를 secret redaction 상태로 기록한다.

## 완료 리뷰

- 상태: 승인됨
- 요청일: 2026-06-15
- 승인일: 2026-06-16
- 완료 근거: Plane Webhook intake 및 normalized trigger dispatch, idempotency 처리가 구현 및 원격 배포되었으며, 2026-06-15 live smoke 테스트를 통해 Plane UI/API에서 새 Backlog 티켓 생성 및 AGENT assignee 지정 시 webhook 202 Accepted 수신 및 Core Task(e3c92411-4056-4b76-a879-66f7904125d0) 생성 및 실행이 시작되는 것을 검증 완료함. 2026-06-16 코드 레벨 재검토에서 receiver/auth/normalize/dispatch/idempotency/config wiring과 관련 테스트를 재확인했고 blocking issue가 없었다.
- 리뷰 필요:
  - [x] 사용자가 완료 결과를 확인했다
  - [x] archive 이동을 승인했다
- 리뷰 코멘트: 코드 레벨 검토 후 종료 처리함.

## 범위 제외

- Plane `Todo -> In Progress` 실행 lifecycle 시작
- Milestone Task를 Plane 하위 티켓으로 생성하거나 동기화하는 구현
- Gito `branch.updated` event producer 또는 proto-socket consumer 구현
- Jira webhook intake
- provider polling 기반 reconcile loop 구현
- 사용자 승인 없는 archive 이동

## 작업 컨텍스트

- 관련 경로: `services/core/internal/http/`, `services/core/internal/workitempipeline/`, `services/core/internal/adapters/plane/`, `services/core/internal/scheduler/`, `services/core/internal/roadmapsyncpipeline/`, `services/core/internal/config/`, `packages/contracts/notes/`, `agent-roadmap/`
- 표준선: Plane webhook payload는 신뢰할 source of truth가 아니라 wakeup hint로 취급하고, NomadCode는 Plane API 재조회와 기존 trigger gate로 실제 실행 여부를 결정한다.
- 선행 작업: `Milestone Work Item Creation Sync`
- 후속 작업: `Gito Branch Event Creation Sync Bridge`, `Milestone Execution Lifecycle Sync`
- 사용자 액션:
  - [x] NomadCode Core webhook endpoint는 dev Plane에서 접근 가능한 `http://toki-labs.com:18010/api/integrations/plane/webhook`로 사용한다.
  - [x] webhook secret은 Plane UI에서 발급받았고 `secrets/nomadcode.dev.sops.yaml`에 SOPS로 저장했다. 구현 시 `PLANE_WEBHOOK_SECRET` env로 주입한다. raw secret 값은 roadmap/docs/log에 남기지 않는다.
  - [x] AGENT assignee로 쓸 Plane 계정/id는 `5d116a77-d3df-4f54-80bf-eca61e0118c4`다.
  - [x] webhook-only live smoke용 기존 Plane 티켓은 `NOMAD-10`, `NOMAD-11`을 사용했다.
  - [x] live smoke 후 Plane UI에서 `Todo` projection 또는 authoring 시작 상태를 확인한다.
- 확인 필요: 없음
- 작은 작업 처리 결과: `webhook-receiver`는 signed fixture/invalid signature/basic-auth bypass 테스트와 dev Core synthetic/live webhook smoke로 확인했다. `plane-registration`은 dev Plane active webhook `e25a2d41-2c9f-4f5f-a0ce-cad6b993853f`, target `http://toki-labs.com:18010/api/integrations/plane/webhook`, `issue=True`, response 202 발송 로그 8건으로 확인했다. 2026-06-15 live smoke에서 Plane issue `93b677f5-7913-4111-ae05-916033ae533b`에 대한 webhook 수신(202 Accepted) 및 Core Task `e3c92411-4056-4b76-a879-66f7904125d0` 기동을 검증함.
- payload fixture: assignee update 샘플은 `services/core/internal/http/testdata/plane_webhook_issue_assignees_updated.json`에 PII redaction 상태로 남겼다. create/state-change 후보는 `plane_webhook_issue_created.json`, `plane_webhook_issue_state_updated.json`으로 추가했고 live create/state 로그 미확보로 관찰된 issue envelope 기반 synthetic candidate임을 `agent-test/plane-dev.md`에 명시했다. normalize helper(`normalizePlaneWebhookPayload`)는 payload를 wakeup hint로만 보고 provider-neutral ref/actor/change hint만 추출한다.
- plane-webhook 후속 plan:
  - `agent-task/m-plane-work-item-webhook-intake/01_payload_normalization/PLAN-local-G06.md`: `[payload-sample]`, `[event-normalize]`
  - `agent-task/m-plane-work-item-webhook-intake/02+01_trigger_dispatch/PLAN-local-G07.md`: `[trigger-dispatch]`
  - `agent-task/m-plane-work-item-webhook-intake/03+02_idempotency/PLAN-cloud-G06.md`: `[loop-idempotency]`
  - `agent-task/m-plane-work-item-webhook-intake/04+01,02,03_live_smoke/PLAN-local-G05.md`: `[live-smoke]`
