<!-- task=m-core-server-boundary-cleanup/01_routes plan=1 tag=ROUTES -->

# Implementation Plan - ROUTES

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

구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, active 파일은 그대로 둔 채 리뷰 준비 상태로 보고한다. 종결 처리, log archive, `complete.log` 작성, task directory archive 이동은 code-review 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경, scope conflict 없이는 진행할 수 없으면 리뷰 stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다.

## 배경

현재 `services/core/internal/httpserver/server.go`는 server 생성, route 등록, manual API router, handler 구현, DTO helper를 한 파일에 함께 둔다. 이 plan은 route 등록과 `/api/v1/` subrouter 구성을 먼저 분리해 이후 handler/DTO 분리의 기반을 만든다. API endpoint 의미와 handler 동작은 유지한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 기준이다. 구현 에이전트는 직접 사용자 prompt를 만들지 않으며, code-review가 사용자 리뷰 요청의 타당성 검증과 실제 `USER_REVIEW.md` 작성을 담당한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/core-server-boundary-cleanup.md`
- Task ids:
  - `routes`: route 등록과 subrouter 구성을 handler 구현에서 분리한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/independent-control-plane/PHASE.md`
- `agent-roadmap/phase/independent-control-plane/milestones/core-server-boundary-cleanup.md`
- `agent-test/local/rules.md`
- `services/core/internal/httpserver/server.go`
- `services/core/internal/httpserver/server_test.go`
- `services/core/internal/cicdstate/store.go`
- `services/core/internal/cicdstate/store_test.go`
- `Makefile`
- `services/core/go.mod`

### 테스트 환경 규칙

- 선택 test_env: `local`.
- `agent-test/local/rules.md` 존재하고 읽음.
- `services/core` 전용 smoke 문서는 없음. local 규칙의 Monorepo Targets 기준으로 `make core-test` 또는 `cd services/core && go test ./...`를 적용한다.
- `<확인 필요>` 값은 없었다.
- fallback 검증 source: root `Makefile`의 `core-test` target.
- Go test cache는 리팩터 검증에서 허용하지 않는다. 최종 검증은 `cd services/core && go test -count=1 ./...`를 사용한다.

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

- route 등록 분리는 endpoint 의미를 바꾸지 않는 내부 리팩터다.
- `TestServerMux`가 `/healthz`, `/readyz`, unknown route를 확인한다.
- `TestHandleCreateJob`, `TestHandleCreateExecution`, `TestHandleExecutionLogsAndArtifacts`, `TestHandleRunnerCicdClaimReportLogsAndArtifacts`, `TestHandleRunnerCancelExecution`, `TestHandleRunnerStatus`, `TestHandleRunnerSelfUpdate`가 `/api/v1/` manual router 경로를 직접 호출한다.
- 새 public API는 없으므로 신규 테스트는 쓰지 않고 기존 HTTP handler tests를 회귀 기준으로 둔다.

### 심볼 참조

- 제거/rename 대상 없음.
- 새 helper 후보: `registerRoutes(mux *http.ServeMux, registry *runnerregistry.Registry, store *cicdstate.Store)`, `handleAPIRouter(store *cicdstate.Store, registry *runnerregistry.Registry)`.
- 기존 `handleRouter`는 tests가 직접 호출한다: `services/core/internal/httpserver/server_test.go:500`, `522`, `536`, `545`, `554`, `574`, `634`, `673`, `700`, `751`, `766`, `788`, `804`, `818`, `827`, `842`, `851`, `859`, `879`, `887`, `907`, `915`, `927`, `936`, `944`, `956`, `968`, `1001`, `1034`, `1047`, `1063`, `1111`, `1206`, `1226`, `1243`, `1265`, `1298`, `1332`, `1341`, `1361`.

### 분할 판단

- split decision policy를 적용했다.
- shared task group: `agent-task/m-core-server-boundary-cleanup`.
- sibling plans:
  - `01_routes`: route 등록과 subrouter 분리. dependencies: none.
  - `02+01_dto`: DTO helper 분리. depends_on: `01_routes`.
  - `03+01,02_handlers`: handler 파일 분리. depends_on: `01_routes`, `02+01_dto`.
- 이 plan은 `01_routes`이므로 predecessor 없음.

### 범위 결정 근거

- 이 plan은 route registration/subrouter 파일 경계만 다룬다.
- handler body 이동, request/response DTO helper 이동, store/domain 동작 변경, auth/storage 추가는 제외한다.
- `services/core/internal/cicdstate/**` clock 수정은 이미 작은 작업으로 직접 처리되었으므로 이 plan 범위가 아니다.

### 빌드 등급

- build: `local-G06`, review: `local-G06`.
- 이유: 한 파일의 routing 경계 리팩터이며 기존 HTTP tests와 fresh Go tests로 회귀 검증 가능하다.

## 구현 체크리스트

- [ ] `services/core/internal/httpserver/routes.go`에 route 등록 helper와 `/api/v1/` subrouter helper를 추가한다.
- [ ] `NewServerWithRegistryAndStore`가 새 route 등록 helper를 호출하도록 바꾸고 endpoint path 목록은 유지한다.
- [ ] `handleRouter` 직접 호출 tests가 깨지지 않도록 기존 symbol 호환을 유지하거나 wrapper를 둔다.
- [ ] `gofmt`를 실행하고 `cd services/core && go test -count=1 ./internal/httpserver`를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [ROUTES-1] Route Registration Boundary

#### 문제

- `services/core/internal/httpserver/server.go:36-48`에서 server 생성자가 모든 endpoint를 직접 등록한다.
- `services/core/internal/httpserver/server.go:416-489`에서 `/api/v1/` manual routing switch가 handler 구현과 같은 파일에 있다.

Before:

```go
// services/core/internal/httpserver/server.go:36
func NewServerWithRegistryAndStore(addr string, registry *runnerregistry.Registry, store *cicdstate.Store) *Server {
	mux := http.NewServeMux()

	mux.HandleFunc("/healthz", handleHealthz)
	mux.HandleFunc("/readyz", handleReadyz)
	mux.HandleFunc("/api/v1/runners/register", handleRunnerRegister(registry))
	mux.HandleFunc("/api/v1/runners/bootstrap-command", handleRunnerBootstrapCommand(registry))
	mux.HandleFunc("/api/v1/runners/{id}/heartbeat", handleRunnerHeartbeat(registry))
	mux.HandleFunc("/api/v1/runners/{id}/disconnect", handleRunnerDisconnect(registry))
	mux.HandleFunc("/api/v1/runners/{id}", handleGetRunner(registry))
	mux.HandleFunc("/bootstrap/oto-agent.sh", handleServeBootstrapScript())
	mux.HandleFunc("/api/v1/", handleRouter(store, registry))
```

#### 해결 방법

- 새 `routes.go`를 같은 package에 추가한다.
- `NewServerWithRegistryAndStore`는 mux 생성 뒤 `registerRoutes(mux, registry, store)`만 호출한다.
- `handleRouter`는 test 호환을 위해 유지하되, 실제 `/api/v1/` 등록은 `handleAPIRouter` 또는 `handleRouter` wrapper를 통해 routes 파일에 둔다.

After:

```go
// services/core/internal/httpserver/server.go
func NewServerWithRegistryAndStore(addr string, registry *runnerregistry.Registry, store *cicdstate.Store) *Server {
	mux := http.NewServeMux()
	registerRoutes(mux, registry, store)
	return &Server{httpServer: &http.Server{Addr: addr, Handler: mux}}
}
```

```go
// services/core/internal/httpserver/routes.go
func registerRoutes(mux *http.ServeMux, registry *runnerregistry.Registry, store *cicdstate.Store) {
	mux.HandleFunc("/healthz", handleHealthz)
	mux.HandleFunc("/readyz", handleReadyz)
	mux.HandleFunc("/api/v1/runners/register", handleRunnerRegister(registry))
	mux.HandleFunc("/api/v1/runners/bootstrap-command", handleRunnerBootstrapCommand(registry))
	mux.HandleFunc("/api/v1/runners/{id}/heartbeat", handleRunnerHeartbeat(registry))
	mux.HandleFunc("/api/v1/runners/{id}/disconnect", handleRunnerDisconnect(registry))
	mux.HandleFunc("/api/v1/runners/{id}", handleGetRunner(registry))
	mux.HandleFunc("/bootstrap/oto-agent.sh", handleServeBootstrapScript())
	mux.HandleFunc("/api/v1/", handleRouter(store, registry))
}
```

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

- [ ] `services/core/internal/httpserver/routes.go` 추가.
- [ ] `services/core/internal/httpserver/server.go`의 `NewServerWithRegistryAndStore` route 등록 제거.
- [ ] `services/core/internal/httpserver/server_test.go`는 기존 tests 유지. symbol 변경이 필요하면 최소 수정.

#### 테스트 작성

- 신규 테스트 작성 생략. 기존 `TestServerMux`와 `/api/v1/` handler tests가 route 동작 회귀를 덮는다.

#### 중간 검증

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

기대 결과: `ok github.com/toki/oto/services/core/internal/httpserver`.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/httpserver/routes.go` | ROUTES-1 |
| `services/core/internal/httpserver/server.go` | ROUTES-1 |
| `services/core/internal/httpserver/server_test.go` | ROUTES-1 |

## 최종 검증

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

기대 결과: 모든 `services/core` package test 통과. Go test cache output은 허용하지 않는다.

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