<!-- task=provider_neutral_plane_entrypoint/02+01_http_plane_entrypoint plan=0 tag=REFACTOR -->

# Plane Compatibility Endpoint Provider-Neutral Refactor

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

`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수다. 구현 후 검증 명령을 실제로 실행하고, 실제 출력과 주요 설계 결정, 계획 대비 변경 사항을 review stub에 기록한 뒤 active 파일을 그대로 둔 채 리뷰 준비 상태로 보고한다. 최종 판정, `.log` 아카이브, `complete.log` 작성, archive 이동은 code-review-skill 전용이다.

## 배경

`POST /api/integrations/plane/tasks`는 유지해야 하는 compatibility entrypoint지만, handler 내부가 Plane DTO와 adapter package를 직접 알고 있다. 선행 `01_pipeline_mapper`가 provider-neutral mapper를 제공하면 HTTP layer는 Plane request body를 `workitem.Ref`로만 변환하고, 조회와 task 생성은 provider-neutral interface/helper로 처리할 수 있다. 이 계획은 새 generic endpoint를 열지 않고 기존 Plane endpoint의 내부 결합만 끊는다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/roadmap/milestones/plane-task-pipeline-design.md`
- `services/core/go.mod`
- `services/core/cmd/server/main.go`
- `services/core/internal/adapters/plane/client.go`
- `services/core/internal/adapters/plane/client_test.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/http/router.go`
- `services/core/internal/workflow/model.go`
- `services/core/internal/workflow/service.go`
- `services/core/internal/workflow/service_test.go`
- `services/core/internal/workitem/provider.go`
- `services/core/internal/workitem/provider_test.go`

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

- `handlers_test.go` currently verifies `buildPlaneCreateTaskInput` with `plane.WorkItem`. It does not verify that `createPlaneTaskRequest` maps to `workitem.Ref` or that handler helpers no longer require Plane DTOs.
- There is no DB-backed HTTP integration test for `CreatePlaneTask`; this plan keeps route/request compatibility and verifies the handler boundary through focused unit tests plus package compile.
- `cmd/server/main.go` passes `*plane.Client` into `NewHandler`; compile coverage must confirm the new `workitem.Reader` interface is satisfied.

### 심볼 참조

- `PlaneWorkItemClient`: defined at `services/core/internal/http/handlers.go:21`, field at `handlers.go:28`, constructor param at `handlers.go:32`. Replace with provider-neutral reader interface.
- `plane.WorkItemRef`: used at `handlers.go:22`, `handlers.go:176-183`, `handlers_test.go:11-20`, `handlers_test.go:41-50`. Remove from `internal/http`.
- `plane.WorkItem`: used at `handlers.go:22`, `handlers.go:188`, `handlers_test.go:16`, `handlers_test.go:47`. Replace with `workitem.WorkItem`.
- `GetWorkItem`: handler call at `handlers.go:105`; keep adapter method but use `FetchWorkItem` through `workitem.Reader` in HTTP.
- `buildPlaneCreateTaskInput`: defined at `handlers.go:188`, tested at `handlers_test.go:11` and `handlers_test.go:41`. Keep helper name if useful, but make signature provider-neutral and delegate to `workitem.BuildCreateTaskInput`.

### 분할 판단

split decision policy를 먼저 평가했다. 이 task는 `provider_neutral_plane_entrypoint` split group의 두 번째 작업이며, directory name `02+01_http_plane_entrypoint`가 runtime dependency를 표현한다. `01_pipeline_mapper`가 `complete.log`를 만든 뒤에만 실행한다. 이 task는 HTTP call-site 전환만 담당하고 foundation mapper 구현은 하지 않는다.

### 범위 결정 근거

- 새 generic endpoint는 만들지 않는다. 기존 `/api/integrations/plane/tasks`를 compatibility entrypoint로 유지한다.
- Plane adapter raw methods, smoke CLI, README 예시는 변경하지 않는다. `cmd/plane-smoke`는 직접 Plane smoke tool이므로 Plane package 의존을 유지한다.
- DB migration, workflow storage, scheduler enqueue 조건은 바꾸지 않는다.
- 이 task 완료 후에도 `cmd/server/main.go`는 Plane adapter를 조립하지만 HTTP package 내부는 `internal/adapters/plane`을 import하지 않는다.

### 빌드 등급

- build lane: `cloud-G06`
- review lane: `cloud-G06`
- 근거: HTTP handler interface와 call-site가 바뀌고, provider-neutral boundary 검색과 package compile 검증이 필요하다.

## 의존 관계 및 구현 순서

- 이 task는 `agent-task/provider_neutral_plane_entrypoint/01_pipeline_mapper/complete.log`가 존재한 뒤 시작한다.
- `01_pipeline_mapper`의 `workitem.TaskCreateInput`과 `workitem.BuildCreateTaskInput` 이름이 계획과 다르면, 이 task의 `계획 대비 변경 사항`에 실제 이름을 기록하고 동일한 의미의 helper를 사용한다.

## 구현 체크리스트

- [ ] `services/core/internal/http`의 Plane adapter import와 DTO 의존을 `workitem.Reader`, `workitem.Ref`, `workitem.WorkItem` 기반으로 전환한다.
- [ ] 기존 Plane compatibility endpoint route와 request/response shape를 유지하고 HTTP helper 테스트를 provider-neutral 타입으로 갱신한다.
- [ ] `gofmt`, provider boundary 검색, 관련 Go 테스트, whitespace 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 구현 항목

### [REFACTOR-1] Plane compatibility endpoint 내부를 provider-neutral reader/mapper로 전환한다

#### 문제

`services/core/internal/http/handlers.go:17-23`에서 HTTP package가 Plane adapter package와 Plane DTO를 직접 import한다. `CreatePlaneTask`도 `handlers.go:99-111`에서 `plane.WorkItemRef`를 만들고 `GetWorkItem`을 호출한 뒤 `buildPlaneCreateTaskInput`으로 Plane DTO를 넘긴다.

Before (`services/core/internal/http/handlers.go:17-23`):

```go
import "github.com/nomadcode/nomadcode-core/internal/adapters/plane"

type PlaneWorkItemClient interface {
	GetWorkItem(ctx context.Context, ref plane.WorkItemRef) (plane.WorkItem, error)
}
```

After (`services/core/internal/http/handlers.go`):

```go
import "github.com/nomadcode/nomadcode-core/internal/workitem"

type WorkItemReader interface {
	FetchWorkItem(ctx context.Context, ref workitem.Ref) (workitem.WorkItem, error)
}
```

#### 해결 방법

- `handlers.go`에서 `internal/adapters/plane` import를 제거하고 `internal/workitem` import를 추가한다.
- `PlaneWorkItemClient`를 `WorkItemReader`로 바꾸고 `Handler` field와 `NewHandler` parameter를 같은 interface로 갱신한다.
- `createPlaneTaskRequest.workItemRef()`는 `workitem.Ref`를 반환한다.

```go
return workitem.NormalizeRef(workitem.Ref{
	Provider: workitem.ProviderID("plane"),
	Tenant:   strings.TrimSpace(input.WorkspaceSlug),
	Project:  strings.TrimSpace(input.ProjectID),
	ID:       strings.TrimSpace(input.WorkItemID),
	URL:      strings.TrimSpace(input.ExternalURL),
})
```

- Plane endpoint에서 workspace/project/work item은 여전히 필수다. `NormalizeRef`는 tenant/project를 요구하지 않으므로 `workItemRef`에서 tenant/project/id를 별도로 검사하고 기존 error message `workspace_slug, project_id, and work_item_id are required`를 유지한다.
- `CreatePlaneTask`는 `h.workItems.FetchWorkItem(r.Context(), ref)`를 호출한다.
- `buildPlaneCreateTaskInput(input createPlaneTaskRequest, item workitem.WorkItem)`는 `workitem.BuildCreateTaskInput`에 위임한다.

```go
return workitem.BuildCreateTaskInput(workitem.TaskCreateInput{
	Ref:     ref,
	StateID: input.StateID,
	Comment: input.Comment,
}, item)
```

- `cmd/server/main.go:87`의 `apphttp.NewHandler(pool, workflowService, planeClient, logger)`는 유지한다. `*plane.Client`가 `FetchWorkItem`을 구현하므로 compile로 확인한다.

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

- [ ] `services/core/internal/http/handlers.go`에서 Plane adapter import를 제거한다.
- [ ] `PlaneWorkItemClient`를 provider-neutral reader interface로 바꾼다.
- [ ] `Handler` field와 `NewHandler` parameter 이름을 provider-neutral하게 바꾼다.
- [ ] `createPlaneTaskRequest.workItemRef`가 `workitem.Ref`를 반환하게 한다.
- [ ] `CreatePlaneTask`가 `FetchWorkItem`과 `workitem.BuildCreateTaskInput` 경로를 쓰게 한다.
- [ ] `services/core/cmd/server/main.go` compile이 깨지지 않게 한다.

#### 테스트 작성

- 수정: `services/core/internal/http/handlers_test.go`
- `TestPlaneTaskRequestBuildsProviderNeutralRef`: Plane request fields가 `workitem.Ref{Provider:"plane", Tenant, Project, ID, URL}`로 trim mapping되는지 확인한다.
- `TestBuildPlaneCreateTaskInputUsesCommentBeforeDescription`: `workitem.WorkItem` fixture로 comment 우선순위, title, source를 검증한다.
- `TestBuildPlaneCreateTaskInputStoresExternalMetadata`: `External.Provider`, `External.ID`, provider-neutral metadata keys `provider`, `tenant`, `project`, `id`, `state_id`, `external_url`을 검증한다.
- 기존 Plane DTO import는 테스트에서 제거한다.

#### 중간 검증

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

기대 결과: HTTP helper 테스트가 provider-neutral 타입으로 fresh run 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/http/handlers.go` | REFACTOR-1 |
| `services/core/internal/http/handlers_test.go` | REFACTOR-1 |
| `services/core/cmd/server/main.go` | REFACTOR-1 compile 확인 only |

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

- `services/core/internal/http`가 `internal/adapters/plane`을 import하지 않는지 확인한다.
- 기존 route `/api/integrations/plane/tasks`가 유지되는지 확인한다.
- Plane request DTO는 compatibility layer로 남되, core task 생성은 `workitem` mapper를 통하는지 확인한다.
- `cmd/plane-smoke`의 Plane package 의존은 direct smoke tool이므로 이번 boundary 검색 범위에서 제외됐는지 확인한다.
- `cmd/server`가 `*plane.Client`를 `NewHandler`에 넘겨도 compile되는지 확인한다.

## 최종 검증

```bash
cd services/core && gofmt -w internal/http/handlers.go internal/http/handlers_test.go
```

기대 결과: 출력 없음. HTTP Go 파일이 gofmt 적용된다.

```bash
cd services/core && ! rg --sort path -n '"github.com/nomadcode/nomadcode-core/internal/adapters/plane"|plane\\.(WorkItem|WorkItemRef|AddCommentInput|UpdateIssueStatusInput|Client|NewClient|Config)' internal/http internal/workitem internal/workflow
```

기대 결과: 출력 없음. HTTP/workitem/workflow가 Plane adapter package나 Plane DTO symbol을 직접 참조하지 않는다.

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

기대 결과: workitem, HTTP, server package가 fresh run/compile로 통과한다.

```bash
git diff --check
```

기대 결과: whitespace error가 없다.

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