# 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로 안전하게 정규화한다. - [ ] [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 응답을 확인한다. - [ ] [event-normalize] Plane webhook payload를 provider-neutral work item ref와 actor/change hint로 정규화하되, 최종 work item 상태는 Plane API 재조회 결과를 사용한다. 검증: payload 필드 누락 또는 stale event가 있어도 fetch 기반 trigger 판정으로 수렴한다. - [ ] [trigger-dispatch] `Backlog + AGENT assignee` 이벤트만 기존 `workitempipeline.CreateTaskFromWorkItem` 경로로 넘기고 나머지는 side effect 없이 ignored 처리한다. 검증: ignored event가 workspace provision 또는 slot reservation을 발생시키지 않는다. - [ ] [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를 가리키는지 확인한다. - [ ] [live-smoke] Plane UI에서 상위 티켓을 만들고 Backlog 상태에서 AGENT에 assign하면 NomadCode task가 생성되고 Milestone authoring flow가 시작된다. 검증: 테스트 Plane 티켓 id, Core task id, authoring run 시작 또는 ignored 사유를 secret redaction 상태로 기록한다. ## 완료 리뷰 - 상태: 없음 - 요청일: 없음 - 완료 근거: 없음 - 리뷰 필요: - [ ] 사용자가 완료 결과를 확인했다 - [ ] 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`을 사용했다. - [ ] live smoke 후 Plane UI에서 `Todo` projection 또는 authoring 시작 상태를 확인한다. - 확인 필요: Plane self-hosted webhook payload의 실제 event type, signature header, custom header 지원 여부를 dev Plane에서 확인해야 한다. - 작은 작업 처리 결과: `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건으로 확인했다. - 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]`