<!-- task=m-external-integration/01_provider_intake_registry plan=0 tag=REFACTOR -->

# Provider Intake Registry Plan

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

`CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 코드 변경, 검증 실행, 실제 stdout/stderr 기록을 끝낸 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 종결 판정, log rename, `complete.log`, task archive 이동은 code-review 스킬만 수행한다. 구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경, secret, 서비스 준비 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 증거 수집으로 닫을 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 External Integration 마일스톤의 남은 핵심 작업은 Plane 전용 intake wiring을 provider-neutral 구조로 바꾸는 것이다. `workitem.Reader`와 `workitempipeline.Service`는 이미 provider-neutral하지만 HTTP handler와 route는 Plane 전용 request/field에 고정되어 있다. 이 작업은 Jira-compatible provider가 같은 work item -> core task pipeline을 재사용할 수 있게 하는 기반 변경이다.

## 사용자 리뷰 요청 흐름

구현 중 차단이 생기면 active `CODE_REVIEW-local-G05.md`의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 그 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- Task ids:
  - `provider-intake-registry`: Plane 전용 task intake handler를 provider-neutral registry/generic route 또는 동등한 조립 구조로 정리
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- `services/core/go.mod`
- `services/core/README.md`
- `services/core/cmd/server/main.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/router.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/workitem/provider.go`
- `services/core/internal/workitem/provider_test.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`
- `services/core/internal/adapters/jira/client.go`
- `services/core/internal/adapters/jira/client_test.go`
- `services/core/internal/adapters/mattermost/client.go`
- `services/core/internal/adapters/mattermost/client_test.go`
- `services/core/internal/adapters/mattermost/task_notification_sink.go`
- `services/core/internal/adapters/mattermost/task_notification_sink_test.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` 존재 및 정독 완료.
- matched profile: `agent-test/local/core-smoke.md` 존재 및 정독 완료.
- 적용 명령: `cd services/core && go test ./...`; core behavior 변경 최소 검증은 `cd services/core && go test ./...`.
- 보조 명령 후보: `cd services/core && go vet ./...`, `cd services/core && go build ./...`.
- `<확인 필요>` 값은 기준 출력 예시뿐이며 이 계획의 필수 명령에는 없다.
- 사전 확인: `cd services/core && go test ./...` 실행 가능함을 확인했다.

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

- 기존 `handlers_test.go`는 Plane 전용 request와 `planeWorkItemTasks` delegation만 검증한다. generic provider route, provider registry lookup, unknown provider error, Jira-compatible intake reuse 테스트가 없다.
- 기존 `workitempipeline/service_test.go`는 single reader pipeline mapping만 검증한다. registry lookup은 아직 없다.
- Plane/Jira adapters의 provider-neutral facets는 유닛 테스트로 검증돼 있어 adapter 내부 변경은 이 계획 범위가 아니다.

### 심볼 참조

- 제거/이름 변경 예정 후보: `planeWorkItemTasks`, `jiraWorkItemTasks`, `createPlaneTaskRequest`, `CreatePlaneTask`.
- 현재 참조:
  - `services/core/internal/http/handlers.go:27-46`
  - `services/core/internal/http/handlers.go:87-130`
  - `services/core/internal/http/handlers.go:175-189`
  - `services/core/internal/http/router.go:33-35`
  - `services/core/internal/http/handlers_test.go:27-119`
  - `services/core/cmd/server/main.go:108-110`

### 분할 판단

- split policy 평가 완료. 공유 API/foundation과 후속 boundary audit가 자연 의존 관계라 split이 필요하다.
- task group: `agent-task/m-external-integration/`.
- sibling plans:
  - `01_provider_intake_registry`: registry/generic route 구현. depends_on=[].
  - `02+01_adapter_boundary`: adapter boundary audit. depends_on=[01], 현재 predecessor `complete.log` 없음.

### 범위 결정 근거

- Plane/Jira/Mattermost adapter 내부 구현은 이미 deterministic unit tests가 있어 제외한다.
- DB schema, workflow lifecycle, scheduler, proto-socket API는 provider intake routing과 직접 관련이 없어 제외한다.
- live Plane/Jira/Mattermost external smoke는 이 작업의 필수 검증이 아니다. registry는 `httptest`/unit tests로 닫는다.
- 기존 Plane endpoint 호환성은 유지한다. 단, 내부 구현은 generic handler로 위임한다.

### 빌드 등급

- build: `local-G06`, review: `local-G05`.
- 이유: HTTP API shape와 provider routing을 바꾸지만 파일 범위와 tests가 명확하고 local Go tests로 재검증 가능하다.

## 구현 체크리스트

- [ ] provider ID -> `WorkItemTaskCreator` registry를 `Handler`에 도입하고 `NewHandler`에서 Plane/Jira readers를 registry로 조립한다.
- [ ] generic work item task request parser와 handler를 만들고, `/api/integrations/{provider}/tasks`가 registry를 통해 pipeline을 호출하게 한다.
- [ ] 기존 `/api/integrations/plane/tasks`는 compatibility alias로 유지하되 generic handler를 재사용하게 한다.
- [ ] `handlers_test.go`에 Plane compatibility, generic Jira-compatible route, unknown/unconfigured provider error, provider mismatch/invalid body 테스트를 추가 또는 갱신한다.
- [ ] `cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline ./internal/workitem`를 통과시킨다.
- [ ] `cd services/core && go test ./...`를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Provider Registry Foundation

문제: `Handler`가 Plane/Jira pipeline을 개별 필드로 보관한다. 새 provider를 추가하려면 handler field와 branch를 계속 늘려야 한다.

Before:

```go
// services/core/internal/http/handlers.go:27
type Handler struct {
	db                 *pgxpool.Pool
	workflow           *workflow.Service
	planeWorkItemTasks WorkItemTaskCreator
	jiraWorkItemTasks  WorkItemTaskCreator
	logger             *slog.Logger
}

func NewHandler(pool *pgxpool.Pool, workflowService *workflow.Service, plane workitem.Reader, jira workitem.Reader, logger *slog.Logger) *Handler {
	h := &Handler{
		db:       pool,
		workflow: workflowService,
		logger:   logger,
	}
	if plane != nil && workflowService != nil {
		h.planeWorkItemTasks = workitempipeline.New(plane, workflowService)
	}
	if jira != nil && workflowService != nil {
		h.jiraWorkItemTasks = workitempipeline.New(jira, workflowService)
	}
	return h
}
```

해결 방법: handler에 `map[workitem.ProviderID]WorkItemTaskCreator` registry를 둔다. `NewHandler`는 기존 signature를 유지하되 `registerWorkItemReader(providerID, reader)` helper로 Plane/Jira를 등록한다. nil reader 또는 nil workflow는 등록하지 않는다. provider lookup은 trim/normalize된 provider ID를 사용한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/http/handlers.go`: registry field와 registration helper 추가.
- [ ] `services/core/internal/http/handlers_test.go`: registry 직접 주입을 쓰는 fake test fixture로 갱신.

테스트 작성: `TestNewHandlerRegistersConfiguredWorkItemProviders` 또는 동등 테스트를 추가해 Plane/Jira reader가 registry에 등록되고 nil reader는 빠지는지 확인한다.

중간 검증:

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

### [REFACTOR-2] Generic Intake Handler And Routes

문제: 현재 route와 request DTO가 Plane 전용이다. Jira-compatible provider가 `workitem.Reader`를 구현해도 provider별 task handler/route를 새로 만들어야 한다.

Before:

```go
// services/core/internal/http/router.go:33
protected.Route("/api", func(r chi.Router) {
	r.Post("/integrations/plane/tasks", handler.CreatePlaneTask)
	r.Post("/tasks", handler.CreateTask)
```

```go
// services/core/internal/http/handlers.go:87
type createPlaneTaskRequest struct {
	WorkspaceSlug string `json:"workspace_slug"`
	ProjectID     string `json:"project_id"`
	WorkItemID    string `json:"work_item_id"`
	StateID       string `json:"state_id"`
	ExternalURL   string `json:"external_url"`
	Comment       string `json:"comment"`
}
```

해결 방법: `createWorkItemTaskRequest`로 교체한다. JSON은 neutral fields `tenant`, `project`, `id`, `external_url`, `state_id`, `comment`를 기본으로 받고, compatibility fields `workspace_slug`, `project_id`, `work_item_id`도 Plane alias에서 계속 받는다. `POST /api/integrations/{provider}/tasks`를 추가하고 `POST /api/integrations/plane/tasks`는 provider=`plane`으로 같은 handler를 호출한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/http/handlers.go`: generic request DTO, provider param parser, registry lookup handler 추가.
- [ ] `services/core/internal/http/router.go`: generic route 추가, Plane compatibility route 유지.
- [ ] `services/core/README.md`: Plane 전용 예시 옆에 provider-neutral endpoint를 짧게 기록한다.

테스트 작성:

- [ ] `TestCreateWorkItemTaskDelegatesToRegisteredProvider` 추가: `/api/integrations/jira/tasks` body `{id:"PROJ-1"}`가 Jira registry entry로 전달된다.
- [ ] `TestCreatePlaneTaskCompatibilityRouteUsesRegistry` 갱신: legacy Plane body가 provider=`plane`으로 전달된다.
- [ ] `TestCreateWorkItemTaskRejectsUnknownProvider` 추가.
- [ ] `TestCreateWorkItemTaskRejectsProviderMismatch` 추가: path provider와 body provider가 다르면 400.

중간 검증:

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

### [REFACTOR-3] Pipeline Contract Preservation

문제: `workitempipeline.Service`는 provider-neutral이지만 registry 전환 후에도 Plane/Jira가 같은 pipeline을 쓰는지 테스트가 handler 쪽에서 보장돼야 한다.

해결 방법: `workitempipeline.Service` 자체는 변경하지 않는다. handler tests에서 fake provider별 pipeline이 받은 `workitempipeline.CreateTaskInput.Ref.Provider`, `StateID`, `Comment`를 확인한다. 필요하면 `workitempipeline/service_test.go`에는 provider가 `jira`여도 metadata/payload가 provider-neutral로 생성되는 작은 테스트만 추가한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/workitempipeline/service_test.go`: Jira-compatible ref mapping test가 필요하면 추가.
- [ ] 기존 `workitem.BuildCreateTaskInput` behavior를 바꾸지 않는다.

테스트 작성: provider-neutral metadata가 `"provider":"jira"`를 보존하는 테스트를 추가한다. 변경이 handler tests만으로 충분하면 이 파일은 건드리지 않고 계획 대비 변경 사항에 사유를 적는다.

중간 검증:

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/http/handlers.go` | REFACTOR-1, REFACTOR-2 |
| `services/core/internal/http/router.go` | REFACTOR-2 |
| `services/core/internal/http/handlers_test.go` | REFACTOR-1, REFACTOR-2 |
| `services/core/internal/workitempipeline/service_test.go` | REFACTOR-3 |
| `services/core/README.md` | REFACTOR-2 |

## 최종 검증

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

`go test ./...`의 cached output은 전체 회귀 스윕에서는 허용한다. 변경 패키지는 앞선 `-count=1` 명령으로 fresh 실행한다.

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