<!-- task=plane_foundation/01_state_metadata plan=0 tag=PLANE -->

# Plane State ID Metadata Plan

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 필수입니다. 구현 후 검증 명령을 실행하고 실제 출력과 구현 메모를 review stub에 기록한 뒤 active 파일을 유지한 채 리뷰 준비 상태로 보고하세요. 최종 판정, log rename, `complete.log`, archive 이동은 code-review skill 전용입니다.

## 배경

`Plane Communication Foundation` 마일스톤에서 external ref 저장 토대는 구현됐지만, Plane state id는 아직 task metadata에 보존되지 않습니다. token 생성/보관/교체 운영법은 이번 작업에서 제외됐고, 이 계획은 실제 smoke에 앞서 `state_id`를 수동 Plane task 생성 경로에 보존하는 좁은 선행 작업입니다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/domain/core/rules.md`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/workflow/model.go`
- `services/core/internal/workflow/service.go`
- `services/core/README.md`
- `agent-ops/roadmap/milestones/plane-thin-e2e-loop.md`

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

- `createPlaneTaskRequest`가 `state_id`를 받지 않아 metadata에 state id를 남길 수 없다. 기존 `TestBuildPlaneCreateTaskInputStoresExternalMetadata`는 workspace/project/work item만 검증하므로 `state_id` assertion을 추가해야 한다.
- `workflow.Service`는 `ExternalRefInput.Metadata`를 그대로 검증/저장하므로 별도 workflow 테스트 추가는 필요 없다.

### 심볼 참조

- renamed/removed symbol: none.
- `rg --sort path -n "createPlaneTaskRequest|buildPlaneCreateTaskInput" services/core` 결과, call site는 `services/core/internal/http/handlers.go`와 `services/core/internal/http/handlers_test.go`에 한정된다.

### 분할 판단

분할 정책을 먼저 평가했다. 공유 task group은 `plane_foundation`이고, sibling은 `01_state_metadata`와 `02+01_plane_smoke`다. `02+01_plane_smoke`는 실제 외부 API mutation을 검증하므로 `01_state_metadata`의 `complete.log` 이후 진행한다.

### 범위 결정 근거

- DB schema는 `external_metadata jsonb`가 이미 있으므로 migration/query/sqlc 변경은 제외한다.
- Plane adapter의 comment/state update 구현은 이미 있으므로 이번 작업에서 변경하지 않는다.
- token 생성/보관/교체 운영법은 사용자 지시로 현재 작업에서 제외한다.
- 자동 enqueue, task 완료/실패 결과의 Plane 발행, webhook 수집은 마일스톤 범위 제외에 남긴다.

### 빌드 등급

build=`local-G04`, review=`local-G04`. HTTP DTO와 metadata builder/test 중심의 좁은 변경이며 검증은 `go test`로 재현 가능하다.

## 구현 체크리스트

- [ ] `createPlaneTaskRequest`에 optional `state_id` 입력을 추가한다.
- [ ] Plane task metadata와 payload의 `plane` 객체에 trimmed `state_id`를 보존한다.
- [ ] `TestBuildPlaneCreateTaskInputStoresExternalMetadata`에 `state_id` assertion을 추가한다.
- [ ] `services/core/README.md`의 Plane task 생성 예시에 `state_id`를 추가하고, 이 값은 저장만 하며 state update는 smoke 작업에서 검증한다고 명확히 쓴다.
- [ ] `go test -count=1 ./internal/http`와 `go test -count=1 ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [PLANE-1] Plane state id를 external metadata에 보존

#### 문제

`services/core/internal/http/handlers.go:78`의 request DTO에는 `state_id`가 없고, `services/core/internal/http/handlers.go:199`의 metadata에는 workspace/project/work item/external URL만 들어간다.

현재 코드:

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

// services/core/internal/http/handlers.go:199
metadata := map[string]string{
	"workspace_slug": ref.WorkspaceSlug,
	"project_id":     ref.ProjectID,
	"work_item_id":   ref.WorkItemID,
	"external_url":   strings.TrimSpace(input.ExternalURL),
}
```

#### 해결 방법

`state_id`는 work item 조회에는 필요 없으므로 optional field로만 받는다. `buildPlaneCreateTaskInput`에서 trim 후 metadata와 payload의 `plane` 객체에 같은 값으로 넣는다.

목표 코드:

```go
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"`
}

stateID := strings.TrimSpace(input.StateID)
metadata := map[string]string{
	"workspace_slug": ref.WorkspaceSlug,
	"project_id":     ref.ProjectID,
	"work_item_id":   ref.WorkItemID,
	"state_id":       stateID,
	"external_url":   strings.TrimSpace(input.ExternalURL),
}
```

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

- [ ] `services/core/internal/http/handlers.go`: `StateID string 'json:"state_id"'` 추가.
- [ ] `services/core/internal/http/handlers.go`: metadata에 `state_id` 추가.
- [ ] `services/core/internal/http/handlers_test.go`: existing metadata test에 `StateID: "state-1"` 입력과 `metadata["state_id"] == "state-1"` assertion 추가.
- [ ] `services/core/README.md`: curl 예시에 `"state_id"` 추가, 현재 endpoint는 state update를 하지 않고 metadata 저장만 한다고 설명.
- [ ] `agent-ops/roadmap/milestones/plane-thin-e2e-loop.md`: 구현 후 해당 하위 작업과 완료 기준을 체크한다.

#### 테스트 작성

작성한다. `services/core/internal/http/handlers_test.go`의 `TestBuildPlaneCreateTaskInputStoresExternalMetadata`가 state id 저장 회귀 테스트를 겸하도록 확장한다. 새 public API field이므로 normal case를 추가하고, required validation은 추가하지 않는다.

#### 중간 검증

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

예상 결과: `ok github.com/nomadcode/nomadcode-core/internal/http`.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/http/handlers.go` | PLANE-1 |
| `services/core/internal/http/handlers_test.go` | PLANE-1 |
| `services/core/README.md` | PLANE-1 |
| `agent-ops/roadmap/milestones/plane-thin-e2e-loop.md` | PLANE-1 |

## 최종 검증

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

예상 결과: 모든 core package 테스트가 통과한다. cache output은 허용하지 않는다.

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