<!-- task=m-plane-work-item-webhook-intake/02+01_trigger_dispatch plan=0 tag=WEBHOOK_DISPATCH -->

# Code Review Reference - WEBHOOK_DISPATCH

> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.

## 개요

date=2026-06-14
task=m-plane-work-item-webhook-intake/02+01_trigger_dispatch, plan=0, tag=WEBHOOK_DISPATCH

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-work-item-webhook-intake.md`
- Task ids:
  - `trigger-dispatch`: `Backlog + AGENT assignee` 이벤트만 기존 `workitempipeline.CreateTaskFromWorkItem` 경로로 넘기고 나머지는 side effect 없이 ignored 처리한다.
- Completion mode: check-on-pass

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 구현 결과를 실제 소스와 대조하고, PASS이면 code-review 절차에 따라 log/complete/archive를 처리한다. 구현 에이전트는 이 섹션을 실행하지 않는다.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [WEBHOOK_DISPATCH-1] Dispatch Configuration | [x] |
| [WEBHOOK_DISPATCH-2] Handler Dispatch Path | [x] |
| [WEBHOOK_DISPATCH-3] Side Effect Guard Verification | [x] |

## 구현 체크리스트

- [x] Plane webhook dispatch 설정을 config/server/handler에 추가하고 `PLANE_CREATION_BACKLOG_STATE_ID`, `PLANE_AGENT_ASSIGNEE_ID`, optional `PLANE_SELF_ACTOR_ID` env를 주입한다. (ref 구성을 위해 `PLANE_WORKSPACE_SLUG`와 optional guard `PLANE_WORKSPACE_ID`를 함께 추가; 아래 `계획 대비 변경 사항` 참고.)
- [x] `ReceivePlaneWebhook`에서 normalized Plane issue event만 `workitempipeline.CreateTaskFromWorkItem`으로 넘기고 ignored event는 side effect 없이 202로 반환한다. 검증: ignored event가 workspace provision 또는 slot reservation을 발생시키지 않는다. (trigger gate는 `workitempipeline`에서 provision/slot 이전에 평가되므로 ignored event는 creator 호출 없이 202; foreign workspace/unbuildable ref/dispatch 미설정도 creator 호출 없이 202 ignored.)
- [x] handler/config tests를 추가해 matching event는 trigger input으로 전달되고 non-matching/ignored event는 task creator를 호출하지 않음을 확인한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active `CODE_REVIEW-*-G??.md`와 `PLAN-*-G??.md`를 `.log`로 아카이브한다.
- [x] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-plane-work-item-webhook-intake/02+01_trigger_dispatch/`로 이동한다.
- [x] PASS이고 task group이 `m-plane-work-item-webhook-intake`이므로 완료 이벤트 메타데이터를 보고한다. roadmap 수정은 런타임 책임이다.

## 계획 대비 변경 사항

- config 필드 2개 추가(계획은 3개 명시): `PlaneWorkspaceSlug`(env `PLANE_WORKSPACE_SLUG`), `PlaneWorkspaceID`(env `PLANE_WORKSPACE_ID`)를 추가했다. 이유: webhook payload는 workspace UUID만 담고 slug가 없는데, `planeWebhookWorkItemEvent.WorkItemRef(slug)`는 dispatch-ready tenant slug가 비어 있으면 ref를 만들 수 없다. slug 주입이 dispatch의 필수 입력이다. `PlaneWorkspaceID`는 optional guard로, payload workspace id가 설정값과 다르면 dispatch 없이 202 ignored로 끝내 잘못된 slug로 Plane API를 치는 것을 막는다. slug/id는 모두 non-secret 값이라 `agent-test/plane-dev.md`와 docker-compose 기본값에만 기록했다.
- 계획 sketch는 handler에서 `errors.Is(err, ErrCreationTriggerIgnored)`를 명시적으로 검사했지만, 기존 `writeServiceError`가 이미 `ErrCreationTriggerIgnored -> 202 {"status":"ignored"}` 매핑을 갖고 있어 중복을 피하려 그 경로를 재사용했다(legacy `CreatePlaneTask`와 동일 동작).
- dispatch helper 시그니처는 `(storage.Task, bool, error)`로, `dispatched=false`는 creator 호출 없이 ignored 처리(미설정/foreign workspace/unbuildable ref)를 의미하도록 했다.
- 필수 handler test 3개 외에 `foreign workspace`, `dispatch config absent` 케이스 2개를 추가해 WEBHOOK_DISPATCH-3의 creator 호출 횟수(미호출) 검증을 보강했다.
- `workitempipeline` 신규 테스트는 추가하지 않았다(계획상 optional). pipeline behavior 변경이 없고, 기존 `TestCreateTaskFromWorkItemIgnoresNonMatchingTrigger`/`TestCreateTaskFromWorkItemIgnoresSelfActor`가 trigger gate와 side-effect 순서를 이미 검증한다.

## 주요 설계 결정

- side-effect guard는 `workitempipeline.CreateTaskFromWorkItem` 내부에 유지한다. trigger 평가(`EvaluateCreationTrigger`)가 `EnsureProjectWorkspace`/`ReserveWorkspaceSlot`보다 먼저 일어나므로, handler는 strict trigger config(Backlog state + AGENT assignee + self-actor guard)만 전달하면 ignored event가 workspace provision/slot reservation을 발생시키지 않는다. webhook payload logic에 Plane current-state 검사를 중복 구현하지 않았다.
- dispatch 미설정/foreign workspace/unbuildable ref는 503이 아니라 202 ignored로 응답한다(계획의 명시 선택). receiver는 도달 가능하지만 trigger config가 없을 때 provider retry storm을 피하기 위함이다.
- workspace slug는 config로 주입한다. webhook payload가 workspace UUID만 제공하고 slug를 담지 않으므로, `.env.plane.local`의 기존 `PLANE_WORKSPACE_SLUG` 관례와 동일한 env로 dispatch-ready tenant를 주입한다.

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- Dispatch 설정이 raw secret을 추가로 노출하지 않고 non-secret ids만 문서화한다.
- Matching event만 creator에 전달되고 ignored event는 creator call 없이 202가 된다.
- `ErrCreationTriggerIgnored`가 provider retry storm을 만들지 않도록 202로 매핑된다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

### WEBHOOK_DISPATCH-1 중간 검증

```bash
$ cd services/core && go test -count=1 ./internal/config
ok  	github.com/nomadcode/nomadcode-core/internal/config	0.002s
```

### WEBHOOK_DISPATCH-2 중간 검증

```bash
$ cd services/core && go test -count=1 ./internal/http
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.007s
```

### WEBHOOK_DISPATCH-3 중간 검증

```bash
$ cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.007s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s
```

### 최종 검증

```bash
$ cd services/core && gofmt -l internal/config/config.go internal/config/config_test.go internal/http/handlers.go internal/http/plane_webhook.go internal/http/plane_webhook_test.go cmd/server/main.go
(빈 출력 = gofmt diff 없음)

$ cd services/core && go test -count=1 ./internal/config ./internal/http ./internal/workitempipeline
ok  	github.com/nomadcode/nomadcode-core/internal/config	0.002s
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.007s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s

# 로컬 전체 core 빌드/테스트도 통과
$ cd services/core && go test -count=1 ./...
ok  	github.com/nomadcode/nomadcode-core/cmd/plane-smoke	0.004s
... (전 패키지 ok) ...
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.004s

$ tar --exclude='./.git' -czf - . | ssh toki@toki-labs.com 'mkdir -p ~/agent-work/nomadcode/services/core && tar -C ~/agent-work/nomadcode/services/core -xzf -'
(exit 0)

$ ssh toki@toki-labs.com 'zsh -lc "cd ~/agent-work/nomadcode/services/core && go test -count=1 ./..."'
ok  	github.com/nomadcode/nomadcode-core/cmd/plane-smoke	0.409s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/a2a	0.782s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/jira	1.201s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/mattermost	1.598s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/openai	1.867s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/plane	2.255s
ok  	github.com/nomadcode/nomadcode-core/internal/authoring	2.608s
ok  	github.com/nomadcode/nomadcode-core/internal/config	2.942s
ok  	github.com/nomadcode/nomadcode-core/internal/gitoevents	3.367s
ok  	github.com/nomadcode/nomadcode-core/internal/gitosync	3.654s
ok  	github.com/nomadcode/nomadcode-core/internal/http	3.707s
ok  	github.com/nomadcode/nomadcode-core/internal/notification	3.930s
ok  	github.com/nomadcode/nomadcode-core/internal/projectsync	3.895s
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	3.605s
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsync	3.809s
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline	3.752s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	5.663s
ok  	github.com/nomadcode/nomadcode-core/internal/storage	3.790s
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	3.735s
ok  	github.com/nomadcode/nomadcode-core/internal/workitem	3.722s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	3.711s
```

## 코드리뷰 결과

- 종합 판정: PASS
- 차원별 평가:
  - correctness: Pass - normalized Plane issue event만 dispatch 대상이 되고, dispatch 미설정/foreign workspace/trigger ignored 경로는 202 ignored로 닫힌다.
  - completeness: Pass - config/server/handler wiring, handler tests, side-effect guard 검증이 계획 범위와 일치한다.
  - test coverage: Pass - required config/handler tests가 추가되었고 `workitempipeline` side-effect guard는 기존 tests로 유지됨이 확인된다.
  - API contract: Pass - public webhook endpoint는 signed payload 인증과 202 ack contract를 유지하고, 신규 env는 non-secret dispatch gate로만 추가되었다.
  - code quality: Pass - debug 출력, stale TODO, formatting 문제 없음.
  - plan deviation: Pass - workspace slug/id config 추가와 malformed relevant event 202 ignored 선택이 구현 스텁에 근거와 함께 기록되어 있고 범위 내로 판단된다.
  - verification trust: Pass - 리뷰 중 `gofmt -l`, focused Go tests, 전체 `go test -count=1 ./...`, `git diff --check`를 재실행해 통과를 확인했다.
- 발견된 문제: 없음
- 다음 단계: PASS - `CODE_REVIEW-local-G07.md`와 `PLAN-local-G07.md`를 log로 아카이브하고 `complete.log` 작성 후 task directory를 `agent-task/archive/2026/06/m-plane-work-item-webhook-intake/02+01_trigger_dispatch/`로 이동한다.
