<!-- task=plane_communication_foundation plan=0 tag=PLANE -->

# Plane Communication Foundation Scope Alignment

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

구현 완료 전 `CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제로 실행하고 stdout/stderr를 기록한 뒤, active 파일은 그대로 둔 상태에서 리뷰 준비 완료만 보고한다. `complete.log` 작성, active 파일 아카이브, 코드리뷰 판정은 code-review skill 전용이다.

## 배경

현재 마일스톤은 Plane pipeline이 아니라 Plane 통신 토대에 집중하도록 재정의되었다. 기존 작업 트리에는 Plane adapter와 외부 참조 저장처럼 유지할 부분이 있지만, 완료/실패 결과를 Plane에 자동 발행하거나 Plane task 생성 즉시 enqueue하는 흐름은 pipeline 설계가 끝난 뒤 다뤄야 한다. 다음 작업은 이미 들어간 구현을 새 범위에 맞춰 정렬하고, 통신 토대 검증 방법을 남기는 것이다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/roadmap/milestones/plane-thin-e2e-loop.md`
- `README.md`
- `services/core/README.md`
- `services/core/docker-compose.yml`
- `services/core/cmd/server/main.go`
- `services/core/internal/adapters/mattermost/client.go`
- `services/core/internal/adapters/plane/client.go`
- `services/core/internal/adapters/plane/client_test.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/http/middleware_test.go`
- `services/core/internal/http/router.go`
- `services/core/internal/notification/model.go`
- `services/core/internal/notification/service.go`
- `services/core/internal/notification/service_test.go`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/internal/storage/store.go`
- `services/core/internal/workflow/model.go`
- `services/core/internal/workflow/service.go`
- `services/core/internal/workflow/service_test.go`
- `services/core/migrations/00001_create_tasks.sql`
- `services/core/migrations/00002_add_task_external_refs.sql`
- `services/core/queries/tasks.sql`

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

- Plane adapter의 path/header/body/error 동작은 `services/core/internal/adapters/plane/client_test.go`가 다룬다. 유지 대상이다.
- 외부 참조 입력 정규화는 `services/core/internal/workflow/service_test.go`가 다룬다. 유지 대상이다.
- Plane work item에서 pending task 입력을 구성하는 helper는 `services/core/internal/http/handlers_test.go`가 다룬다. `enqueue`, completed/failed state metadata 제거에 맞춰 테스트를 수정해야 한다.
- 자동 Plane 완료/실패 발행은 `services/core/internal/notification/service_test.go`가 다루지만 새 마일스톤 범위 밖이다. 해당 테스트는 삭제하거나 범위 내 테스트로 재작성하지 않는다.
- 실제 Plane write smoke는 unit test가 아니라 수동 외부 검증이다. token secret은 repo에 남기지 않고, 실행 여부와 결과만 `CODE_REVIEW-cloud-G06.md`에 기록한다.

### 심볼 참조

- 제거 예정 `notification.PlaneClient`: 정의 `services/core/internal/notification/service.go:16`; 사용 `service.go:23`, `service.go:27`.
- 제거 예정 `notification.NotifyTaskFailed`: 정의 `services/core/internal/notification/service.go:54`; 사용 `services/core/internal/scheduler/jobs.go:312`, `services/core/internal/notification/service_test.go:37`.
- 제거 예정 `notifyPlane`, `planeMetadata`, `parsePlaneMetadata`, `formatPlaneComment`, notification `stringValue`: 정의 `services/core/internal/notification/service.go:61-145`; 호출 `service.go:40`, `service.go:51`, `service.go:58`.
- 되돌릴 `notification.NewService` signature: 정의 `services/core/internal/notification/service.go:27`; 호출 `services/core/cmd/server/main.go:56`, `services/core/internal/notification/service_test.go:14`, `service_test.go:33`, `service_test.go:54`, `service_test.go:74`.
- 제거 예정 `createPlaneTaskRequest.Enqueue`: 정의 `services/core/internal/http/handlers.go:84`; 사용 `handlers.go:125-133`; 문서 예시 `services/core/README.md:104`.
- 제거 예정 `CompletedStateID`, `FailedStateID`: 정의 `services/core/internal/http/handlers.go:85-86`; metadata 사용 `handlers.go:215-216`; 테스트 `services/core/internal/http/handlers_test.go:46-47`, `handlers_test.go:69-70`; 문서 `services/core/README.md:105-106`.

### 분할 판단

Split decision policy를 먼저 평가했다. 이 작업은 여러 파일을 건드리지만 목적은 하나다: 기존 변경분을 Plane 통신 토대 범위로 맞추는 정렬 작업이다. 새 shared API를 먼저 만들고 후속 call-site rollout을 하는 구조가 아니며, storage/migration/adapter 기반은 유지하고 자동 publish/즉시 enqueue만 제거한다. 테스트와 문서도 같은 scope boundary를 검증하기 위해 함께 바뀌어야 하므로 single plan이 더 안전하다.

### 범위 결정 근거

- 유지: `services/core/internal/adapters/plane/`의 work item 조회/comment/state update client, `PLANE_BASE_URL`/`PLANE_TOKEN` 설정, `X-Api-Key` 계약, 외부 참조 저장 schema/query/workflow.
- 유지: manual `POST /api/integrations/plane/tasks` endpoint는 Plane work item 조회와 external ref 저장 토대 검증용으로 남긴다.
- 제외: scheduler/notification에서 task 완료/실패 결과를 Plane comment/status로 자동 발행하는 정책. 마일스톤의 범위 제외 항목이다.
- 제외: Plane work item import와 task enqueue를 한 요청에서 이어붙이는 pipeline성 흐름. `/api/tasks/{id}/enqueue`는 기존 task API로 남기되 Plane import endpoint에서는 제거한다.
- 제외: remote compose restart. compose/env 문서와 파일 수정은 가능하지만 컨테이너 재시작은 하지 않는다.

### 빌드 등급

build=`cloud-G06`, review=`cloud-G06`. auth/protocol/storage 경계와 HTTP/worker/doc call-site를 함께 다루지만 구현 방향은 좁고 검증 명령이 명확하다.

## 구현 체크리스트

- [ ] 자동 Plane 완료/실패 발행 경로를 notification/scheduler/main에서 제거하고 Mattermost notification stub 동작은 유지한다.
- [ ] Plane work item import endpoint를 pending task 생성과 external ref 저장에 한정하고, enqueue/state-policy 필드를 제거한다.
- [ ] README와 compose/env 설명을 Plane 통신 토대 기준으로 정리하고 secret 값을 기록하지 않는다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 결과를 `CODE_REVIEW-cloud-G06.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 구현 항목

### [PLANE-1] 자동 Plane 결과 발행 제거

#### 문제

`services/core/internal/notification/service.go:16-19`가 Plane comment/status 발행 interface를 notification service에 넣고, `service.go:35-58`이 완료/실패 notification에서 `notifyPlane`을 호출한다. `services/core/internal/scheduler/jobs.go:61-75`와 `jobs.go:311-325`도 task 결과에 external ref를 실어 자동 발행을 트리거한다. 이는 새 마일스톤의 "task 완료/실패 결과를 Plane으로 자동 발행하는 정책 확정" 제외 범위를 침범한다.

Before `services/core/internal/notification/service.go:16-58`:

```go
type PlaneClient interface {
	AddComment(ctx context.Context, input plane.AddCommentInput) error
	UpdateIssueStatus(ctx context.Context, input plane.UpdateIssueStatusInput) error
}

type Service struct {
	mattermost *mattermost.Client
	plane      PlaneClient
	logger     *slog.Logger
}

func NewService(mattermostClient *mattermost.Client, planeClient PlaneClient, logger *slog.Logger) *Service {
	return &Service{
		mattermost: mattermostClient,
		plane:      planeClient,
		logger:     logger,
	}
}
```

After:

```go
type Service struct {
	mattermost *mattermost.Client
	logger     *slog.Logger
}

func NewService(mattermostClient *mattermost.Client, logger *slog.Logger) *Service {
	return &Service{
		mattermost: mattermostClient,
		logger:     logger,
	}
}
```

Before `services/core/internal/scheduler/jobs.go:61-75`:

```go
err = w.Notifications.NotifyTaskCompleted(ctx, notification.TaskNotification{
	TaskID:           task.ID,
	Title:            task.Title,
	Status:           task.Status,
	Message:          message,
	ExternalProvider: task.ExternalProvider,
	ExternalID:       task.ExternalID,
	ExternalURL:      task.ExternalUrl,
	ExternalMetadata: task.ExternalMetadata,
})
```

After:

```go
err = w.Notifications.NotifyTaskCompleted(ctx, notification.TaskNotification{
	TaskID:  task.ID,
	Title:   task.Title,
	Status:  task.Status,
	Message: message,
})
```

#### 해결 방법

- `notification.TaskNotification`을 기존 message-oriented fields로 되돌린다.
- `notification.Service`에서 Plane interface, Plane field, `NotifyTaskFailed`, Plane metadata parsing/comment formatting helpers를 제거한다.
- `scheduler.TaskWorker`는 완료 시 Mattermost-style notification만 보내고, 실패 시 `FailTask`만 수행한다.
- `cmd/server/main.go`는 `planeClient`를 HTTP handler에는 계속 넘기되 `notification.NewService`에는 넘기지 않는다.
- `services/core/internal/notification/service_test.go`는 자동 Plane publish 전용 테스트 파일이므로 삭제한다.

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

- [ ] `services/core/internal/notification/model.go`: `encoding/json`, `Error`, `External*` fields 제거.
- [ ] `services/core/internal/notification/service.go`: Plane import/interface/helpers/failed notification 제거, `NewService` signature 복구.
- [ ] `services/core/internal/notification/service_test.go`: 파일 삭제.
- [ ] `services/core/internal/scheduler/jobs.go`: completion notification payload 축소, failure notification 제거.
- [ ] `services/core/cmd/server/main.go`: `notification.NewService(mattermostClient, logger)`로 호출 변경.

#### 테스트 작성

새 unit test는 작성하지 않는다. 제거 대상은 아직 설계되지 않은 Plane result publish 정책이며, 남는 Mattermost stub은 기존 compile과 scheduler tests로 보호한다. 대신 `rg` 검증으로 제거되어야 할 symbol이 남지 않았는지 확인한다.

#### 중간 검증

```bash
rg --sort path -n "NotifyTaskFailed|notifyPlane|PlaneClient|planeMetadata|ExternalProvider|ExternalMetadata" services/core/internal/notification services/core/internal/scheduler
```

예상 결과: 출력 없음.

### [PLANE-2] Plane import endpoint를 pending task 생성에 한정

#### 문제

`services/core/internal/http/handlers.go:78-87`의 Plane import request가 `enqueue`, `completed_state_id`, `failed_state_id`를 받는다. `handlers.go:125-133`은 Plane import와 enqueue를 한 요청에서 이어 붙이고, `handlers.go:210-217`은 완료/실패 state policy용 id를 task metadata에 저장한다. 현재 단계에서는 pipeline과 결과 정책을 확정하지 않았으므로 manual import는 pending task 생성과 external ref 저장까지만 해야 한다.

Before `services/core/internal/http/handlers.go:78-87`:

```go
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"`
	Enqueue          bool   `json:"enqueue"`
	CompletedStateID string `json:"completed_state_id"`
	FailedStateID    string `json:"failed_state_id"`
}
```

After:

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

Before `services/core/internal/http/handlers.go:125-133`:

```go
status := stdhttp.StatusCreated
if input.Enqueue {
	task, err = h.workflow.EnqueueTask(r.Context(), task.ID)
	if err != nil {
		h.writeServiceError(w, err)
		return
	}
	status = stdhttp.StatusOK
}
```

After:

```go
status := stdhttp.StatusCreated
```

#### 해결 방법

- `createPlaneTaskRequest`에서 `Enqueue`, `CompletedStateID`, `FailedStateID`를 제거한다.
- `CreatePlaneTask`는 work item detail을 조회해 `workflow.CreateTask`만 호출하고 pending task를 반환한다.
- `buildPlaneCreateTaskInput` metadata는 `workspace_slug`, `project_id`, `work_item_id`, `external_url`만 보존한다.
- `services/core/internal/http/handlers_test.go`를 새 metadata shape에 맞춘다.
- route 자체는 `POST /api/integrations/plane/tasks`로 유지한다. 이 endpoint는 자동 pipeline이 아니라 수동 import foundation이다.

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

- [ ] `services/core/internal/http/handlers.go`: request fields, enqueue block, state-policy metadata 제거.
- [ ] `services/core/internal/http/handlers_test.go`: state id assertion 제거, pending import semantics에 맞게 test 이름/fixture 수정.
- [ ] `services/core/internal/http/router.go`: route 유지 여부만 확인.
- [ ] `services/core/internal/http/middleware_test.go`: `NewHandler` signature 변경 영향이 없는지 확인.

#### 테스트 작성

기존 `TestBuildPlaneCreateTaskInputUsesCommentBeforeDescription`는 유지한다. `TestBuildPlaneCreateTaskInputStoresExternalMetadata`는 state-policy field가 저장되지 않는 새 기대값으로 수정한다.

#### 중간 검증

```bash
rg --sort path -n "CompletedStateID|FailedStateID|completed_state_id|failed_state_id|\"enqueue\"" services/core/internal/http services/core/README.md
```

예상 결과: 출력 없음.

### [PLANE-3] 문서와 smoke 절차를 통신 토대 기준으로 정리

#### 문제

`services/core/README.md:93-110`은 Plane work item import 후 즉시 enqueue하고 완료/실패 결과가 Plane comment/status로 발행되는 흐름을 설명한다. 이 설명은 pipeline 설계 전제라 현재 마일스톤과 맞지 않는다. 다만 `services/core/docker-compose.yml:25-26`의 `PLANE_BASE_URL`, `PLANE_TOKEN` env passthrough와 adapter의 `X-Api-Key` 계약은 유지해야 한다.

Before `services/core/README.md:93-110`:

```md
Plane work item에서 task를 만들고 바로 enqueue하는 예시:
...
    "enqueue": true,
    "completed_state_id": "...",
    "failed_state_id": "..."
...
성공 케이스는 `GET /api/tasks/{id}`가 `completed`가 된 뒤 Plane work item comment와 상태를 확인합니다.
```

After:

```md
Plane work item에서 pending task를 수동 생성하는 예시:
...
현재 endpoint는 Plane work item 조회와 external ref 저장만 검증합니다. 자동 enqueue와 완료/실패 결과 발행은 pipeline 설계 이후 별도 마일스톤에서 다룹니다.
```

#### 해결 방법

- root README의 env 설명은 유지하되, 필요하면 "자동 pipeline 아님"으로 문구를 좁힌다.
- `services/core/README.md`의 Plane 예시는 pending task 생성으로 바꾸고 `enqueue`, completed/failed state id를 제거한다.
- Plane direct smoke 절차는 secret 값을 남기지 않는 env 기반 curl 예시로 둔다.
- compose 파일은 `PLANE_BASE_URL: ${PLANE_BASE_URL:-}`, `PLANE_TOKEN: ${PLANE_TOKEN:-}` 형태를 유지한다. remote container restart는 하지 않는다.

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

- [ ] `README.md`: Plane env 설명이 자동 pipeline을 암시하지 않는지 확인.
- [ ] `services/core/README.md`: Plane import 예시와 한계 설명 수정.
- [ ] `services/core/docker-compose.yml`: env passthrough 유지 확인, secret literal 금지.

#### 테스트 작성

문서 변경이므로 unit test는 작성하지 않는다. 대신 `rg`로 pipeline성 예시와 secret literal이 남지 않았는지 확인한다.

#### 중간 검증

```bash
rg --sort path -n "PLANE_BASE_URL|PLANE_TOKEN|X-Api-Key|work-items" README.md services/core/README.md services/core/docker-compose.yml services/core/internal/adapters/plane/client.go
```

예상 결과: env/header/work item endpoint 설명과 구현 위치만 출력된다. secret 값은 출력되지 않아야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/notification/model.go` | PLANE-1 |
| `services/core/internal/notification/service.go` | PLANE-1 |
| `services/core/internal/notification/service_test.go` | PLANE-1 |
| `services/core/internal/scheduler/jobs.go` | PLANE-1 |
| `services/core/cmd/server/main.go` | PLANE-1 |
| `services/core/internal/http/handlers.go` | PLANE-2 |
| `services/core/internal/http/handlers_test.go` | PLANE-2 |
| `services/core/internal/http/router.go` | PLANE-2 |
| `services/core/internal/http/middleware_test.go` | PLANE-2 |
| `README.md` | PLANE-3 |
| `services/core/README.md` | PLANE-3 |
| `services/core/docker-compose.yml` | PLANE-3 |

## 최종 검증

Go test cache는 허용하지 않는다. fresh execution을 위해 `-count=1`을 사용한다.

```bash
cd services/core && ./bin/sqlc
```

예상 결과: 성공. 생성 파일 diff는 schema/query와 일치해야 한다.

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

예상 결과: 성공.

```bash
cd services/core && ./bin/test
```

예상 결과: 성공.

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

예상 결과: 출력 없음.

```bash
rg --sort path -n "NotifyTaskFailed|notifyPlane|PlaneClient|planeMetadata|CompletedStateID|FailedStateID|completed_state_id|failed_state_id|\"enqueue\"" services/core README.md
```

예상 결과: 출력 없음. 단, 기존 일반 task enqueue route 문구는 검색 패턴에 걸리지 않아야 한다.

수동 Plane smoke는 `PLANE_TOKEN`이 shell에 설정되어 있을 때만 실행한다. token 값은 출력하거나 파일에 저장하지 않는다. 기존 smoke work item을 재사용하려면 아래 기본값을 사용하고, 새 smoke item을 쓰면 env만 바꾼다.

```bash
PLANE_BASE_URL="${PLANE_BASE_URL:-https://plane.toki-labs.com}" \
PLANE_WORKSPACE_SLUG="${PLANE_WORKSPACE_SLUG:-general}" \
PLANE_PROJECT_ID="${PLANE_PROJECT_ID:-a6beb42f-7a8a-410c-b50f-ea3ca94828f3}" \
PLANE_WORK_ITEM_ID="${PLANE_WORK_ITEM_ID:-0e6334c6-704c-4679-9149-e130439685e3}" \
sh -c 'curl -fsS -o /tmp/plane-work-item.json -w "%{http_code}\n" -H "X-Api-Key: ${PLANE_TOKEN:?set PLANE_TOKEN}" "$PLANE_BASE_URL/api/v1/workspaces/$PLANE_WORKSPACE_SLUG/projects/$PLANE_PROJECT_ID/work-items/$PLANE_WORK_ITEM_ID"'
```

예상 결과: `200`.

```bash
PLANE_BASE_URL="${PLANE_BASE_URL:-https://plane.toki-labs.com}" \
PLANE_WORKSPACE_SLUG="${PLANE_WORKSPACE_SLUG:-general}" \
PLANE_PROJECT_ID="${PLANE_PROJECT_ID:-a6beb42f-7a8a-410c-b50f-ea3ca94828f3}" \
PLANE_WORK_ITEM_ID="${PLANE_WORK_ITEM_ID:-0e6334c6-704c-4679-9149-e130439685e3}" \
sh -c 'curl -fsS -o /tmp/plane-comment.json -w "%{http_code}\n" -X POST -H "Content-Type: application/json" -H "X-Api-Key: ${PLANE_TOKEN:?set PLANE_TOKEN}" -d "{\"comment_html\":\"<p>NomadCode Plane communication foundation smoke</p>\"}" "$PLANE_BASE_URL/api/v1/workspaces/$PLANE_WORKSPACE_SLUG/projects/$PLANE_PROJECT_ID/work-items/$PLANE_WORK_ITEM_ID/comments/"'
```

예상 결과: `200` 또는 `201`.

```bash
PLANE_BASE_URL="${PLANE_BASE_URL:-https://plane.toki-labs.com}" \
PLANE_WORKSPACE_SLUG="${PLANE_WORKSPACE_SLUG:-general}" \
PLANE_PROJECT_ID="${PLANE_PROJECT_ID:-a6beb42f-7a8a-410c-b50f-ea3ca94828f3}" \
PLANE_WORK_ITEM_ID="${PLANE_WORK_ITEM_ID:-0e6334c6-704c-4679-9149-e130439685e3}" \
PLANE_DONE_STATE_ID="${PLANE_DONE_STATE_ID:-ea2e5b48-8bf1-4723-b749-de7723be41e9}" \
sh -c 'curl -fsS -o /tmp/plane-state.json -w "%{http_code}\n" -X PATCH -H "Content-Type: application/json" -H "X-Api-Key: ${PLANE_TOKEN:?set PLANE_TOKEN}" -d "{\"state\":\"$PLANE_DONE_STATE_ID\"}" "$PLANE_BASE_URL/api/v1/workspaces/$PLANE_WORKSPACE_SLUG/projects/$PLANE_PROJECT_ID/work-items/$PLANE_WORK_ITEM_ID/"'
```

예상 결과: `200`.

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