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

# Plan - WEBHOOK_DISPATCH

## 이 파일을 읽는 구현 에이전트에게

구현 완료 전 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 반드시 채운다. 구현 중 사용자만 결정할 수 있는 외부 환경, secret, scope 충돌이 생기면 리뷰 stub의 `사용자 리뷰 요청` 섹션에 증거와 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

Plane webhook receiver는 현재 인증된 요청을 받지만 creation pipeline을 호출하지 않는다. 기존 HTTP task creation route는 `workitempipeline.CreateTaskFromWorkItem`에 trigger state/assignee/self actor를 전달하는 경로가 이미 있다. 이 작업은 normalize 결과를 그 경로에 연결하되, ignored event는 workspace provision/slot reservation 없이 202로 끝나게 한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 직접 사용자 프롬프트는 금지하며, code-review가 USER_REVIEW 작성 여부를 판단한다.

## 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

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/plane-dev.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-work-item-webhook-intake.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `services/core/internal/http/plane_webhook.go`
- `services/core/internal/http/plane_webhook_test.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `services/core/cmd/server/main.go`
- `services/core/internal/workitempipeline/service.go`
- `services/core/internal/workitempipeline/service_test.go`
- `services/core/internal/adapters/plane/client.go`
- `services/core/internal/adapters/plane/client_test.go`

### 테스트 환경 규칙

- `test_env=local`로 적용한다.
- `agent-test/local/rules.md`와 `agent-test/local/core-smoke.md`를 읽었다.
- core 변경이므로 최종 검증은 원격 기본 checkout에서 `zsh -lc "cd ~/agent-work/nomadcode/services/core && go test ./..."`를 실행한다.
- contracts 파일은 수정하지 않으므로 contracts smoke는 적용하지 않는다.
- `<확인 필요>` 값은 없다.

### 테스트 커버리지 공백

- `services/core/internal/http/handlers_test.go:738`는 provider-neutral task route가 trigger를 전달하는지만 확인한다.
- `services/core/internal/workitempipeline/service_test.go:456`은 wrong state/missing assignee ignored 시 side effect가 없음을 확인한다.
- `ReceivePlaneWebhook`에서 normalize 결과를 `workItemProviders["plane"]`로 dispatch하는 테스트는 없다.
- config에는 `PLANE_WEBHOOK_SECRET`만 있고 `Backlog` state id, AGENT assignee id, self actor id 설정은 없다.

### 심볼 참조

- renamed/removed symbol 없음.

### 분할 판단

- split decision policy를 먼저 평가했다.
- shared task group: `m-plane-work-item-webhook-intake`
- 이 subtask directory `02+01_trigger_dispatch`는 predecessor `01`을 요구한다.
- predecessor `01` 상태: `agent-task/m-plane-work-item-webhook-intake/01_payload_normalization/complete.log` 없음. 구현 시작 전 `01_payload_normalization` PASS/archive 또는 active complete.log가 필요하다.

### 범위 결정 근거

- 이 작업은 webhook-to-pipeline dispatch와 설정 주입까지만 다룬다.
- payload normalize helper는 `01_payload_normalization`의 산출물을 사용한다.
- DB unique constraint, duplicate delivery, retry idempotency는 `03+02_idempotency`에서 다룬다.
- remote live smoke와 Plane UI evidence는 `04+01,02,03_live_smoke`에서 다룬다.

### 빌드 등급

- `local-G07`: HTTP handler, config, server wiring, pipeline call boundary를 함께 만지지만 기존 trigger gate와 focused handler tests로 검증 가능하다.

## 의존 관계 및 구현 순서

- `01_payload_normalization`이 `complete.log`를 생성한 뒤 시작한다.
- 이 plan은 directory name의 predecessor `+01`만 의존한다. 다른 숨은 의존성을 추가하지 않는다.

## 구현 체크리스트

- [ ] Plane webhook dispatch 설정을 config/server/handler에 추가하고 `PLANE_CREATION_BACKLOG_STATE_ID`, `PLANE_AGENT_ASSIGNEE_ID`, optional `PLANE_SELF_ACTOR_ID` env를 주입한다.
- [ ] `ReceivePlaneWebhook`에서 normalized Plane issue event만 `workitempipeline.CreateTaskFromWorkItem`으로 넘기고 ignored event는 side effect 없이 202로 반환한다. 검증: ignored event가 workspace provision 또는 slot reservation을 발생시키지 않는다.
- [ ] handler/config tests를 추가해 matching event는 trigger input으로 전달되고 non-matching/ignored event는 task creator를 호출하지 않음을 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [WEBHOOK_DISPATCH-1] Dispatch Configuration

### 문제

`services/core/internal/config/config.go:28`에는 Plane base/token/webhook secret만 있고, trigger gate에 필요한 Backlog state id와 AGENT assignee id 설정이 없다. `services/core/cmd/server/main.go:146`은 secret만 handler에 주입한다.

### 해결 방법

Before:

```go
// services/core/internal/config/config.go:28
PlaneBaseURL       string
PlaneToken         string
PlaneWebhookSecret string
```

After:

```go
PlaneBaseURL                    string
PlaneToken                      string
PlaneWebhookSecret              string
PlaneCreationBacklogStateID     string
PlaneAgentAssigneeID            string
PlaneSelfActorID                string
```

Add env loading:

```go
PlaneCreationBacklogStateID: firstEnv("PLANE_CREATION_BACKLOG_STATE_ID", "PLANE_BACKLOG_STATE_ID"),
PlaneAgentAssigneeID:        os.Getenv("PLANE_AGENT_ASSIGNEE_ID"),
PlaneSelfActorID:            os.Getenv("PLANE_SELF_ACTOR_ID"),
```

Wire a handler method such as `SetPlaneWebhookDispatchConfig`.

### 수정 파일 및 체크리스트

- [ ] `services/core/internal/config/config.go`: add fields/env loading.
- [ ] `services/core/internal/config/config_test.go`: add env test.
- [ ] `services/core/cmd/server/main.go`: pass config to handler.
- [ ] `services/core/docker-compose.yml`: expose non-secret ids with defaults.
- [ ] `agent-test/plane-dev.md`: document non-secret env values using existing Backlog and AGENT ids.

### 테스트 작성

- Required config test:
  - `TestConfigLoadsPlaneWebhookDispatchEnv`

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/config
```

Expected: package passes.

## [WEBHOOK_DISPATCH-2] Handler Dispatch Path

### 문제

`services/core/internal/http/plane_webhook.go:60` unmarshals payload and `:84` returns accepted. It does not use `Handler.workItemProviders` from `services/core/internal/http/handlers.go:32`, so a valid Plane event cannot start the existing creation sync path.

### 해결 방법

Before:

```go
// services/core/internal/http/plane_webhook.go:81
h.logger.Info("plane webhook received", attrs...)
writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "accepted"})
```

After:

```go
event, ok, err := normalizePlaneWebhookPayload(payload)
if err != nil { writeError(w, stdhttp.StatusBadRequest, "invalid plane webhook payload"); return }
if !ok { writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status":"ignored"}); return }

task, err := h.dispatchPlaneWebhookWorkItem(r.Context(), event)
if errors.Is(err, workitempipeline.ErrCreationTriggerIgnored) {
	writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status":"ignored"})
	return
}
if err != nil { h.writeServiceError(w, err); return }
writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status":"accepted","task_id":task.ID})
```

Dispatch input must set:

```go
Trigger: workitempipeline.CreationTrigger{
	RequiredStateID:    h.planeWebhookDispatch.BacklogStateID,
	RequiredAssigneeID: h.planeWebhookDispatch.AgentAssigneeID,
	Actor:              event.ActorID,
	SelfActor:          h.planeWebhookDispatch.SelfActorID,
}
```

### 수정 파일 및 체크리스트

- [ ] `services/core/internal/http/handlers.go`: add dispatch config field.
- [ ] `services/core/internal/http/plane_webhook.go`: add dispatch helper and error mapping.
- [ ] Ensure missing dispatch config yields 202 ignored or 503 only by explicit choice recorded in review. Prefer 202 ignored when receiver is reachable but trigger config absent to avoid provider retry storms.

### 테스트 작성

- Required handler tests:
  - `TestReceivePlaneWebhookDispatchesMatchingIssueEvent`
  - `TestReceivePlaneWebhookIgnoresIrrelevantEventWithoutCreatorCall`
  - `TestReceivePlaneWebhookReturnsAcceptedWhenCreationTriggerIgnored`

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/http
```

Expected: package passes.

## [WEBHOOK_DISPATCH-3] Side Effect Guard Verification

### 문제

`services/core/internal/workitempipeline/service.go:103` fetches item and `:109` evaluates trigger before provision/slot reservation at `:113`, but webhook handler tests do not prove ignored webhook events avoid dispatch or map ignored trigger to 202.

### 해결 방법

Keep side-effect guard inside `workitempipeline`; handler should only pass strict trigger config and map `ErrCreationTriggerIgnored` to accepted/ignored. Do not duplicate Plane current-state checks in webhook payload logic.

### 수정 파일 및 체크리스트

- [ ] `services/core/internal/http/plane_webhook_test.go`: assert fake creator call counts.
- [ ] `services/core/internal/workitempipeline/service_test.go`: only add tests if dispatch requires new trigger semantics; otherwise rely on existing `TestCreateTaskFromWorkItemIgnoresNonMatchingTrigger`.

### 테스트 작성

- Handler tests required.
- Workitempipeline tests can be skipped if no pipeline behavior changes; record skip reason in review.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline
```

Expected: packages pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/config/config.go` | WEBHOOK_DISPATCH-1 |
| `services/core/internal/config/config_test.go` | WEBHOOK_DISPATCH-1 |
| `services/core/cmd/server/main.go` | WEBHOOK_DISPATCH-1 |
| `services/core/docker-compose.yml` | WEBHOOK_DISPATCH-1 |
| `agent-test/plane-dev.md` | WEBHOOK_DISPATCH-1 |
| `services/core/internal/http/handlers.go` | WEBHOOK_DISPATCH-2 |
| `services/core/internal/http/plane_webhook.go` | WEBHOOK_DISPATCH-2 |
| `services/core/internal/http/plane_webhook_test.go` | WEBHOOK_DISPATCH-2, WEBHOOK_DISPATCH-3 |
| `services/core/internal/workitempipeline/service_test.go` | WEBHOOK_DISPATCH-3, optional |

## 최종 검증

```bash
cd services/core && gofmt -w 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
cd services/core && go test -count=1 ./internal/config ./internal/http ./internal/workitempipeline
tar -C services/core -czf - . | ssh toki@toki-labs.com 'mkdir -p ~/agent-work/nomadcode/services/core && tar -C ~/agent-work/nomadcode/services/core -xzf -'
ssh toki@toki-labs.com 'zsh -lc "cd ~/agent-work/nomadcode/services/core && go test -count=1 ./..."'
```

Expected: local focused tests pass and remote full core tests pass.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
