<!-- task=m-control-plane-separation-migration/05+04_core_cicd_state plan=0 tag=CICD_STATE -->

# Plan - CICD_STATE

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

구현 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 변경 사유, 검증 출력으로 채운다. 구현 중 사용자만 결정할 수 있는 범위 변경이나 외부 환경 준비가 필요하면 리뷰 stub의 `사용자 리뷰 요청` 섹션에 근거를 적고 멈춘다. 직접 사용자에게 질문하거나 `USER_REVIEW.md`, `complete.log`, archive 로그를 만들지 않는다. 명령 재실행으로 해소 가능한 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 OTO Server는 runner 등록, heartbeat, bootstrap만 소유한다. `oto-owns-cicd`를 완료하려면 먼저 서버 내부에 job, execution, log, artifact 상태를 iop 없이 저장하고 조회하는 최소 API가 있어야 한다. 이 계획은 runner dispatch 구현 전, 서버 상태 모델과 HTTP 계약만 닫는다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 사용자에게 직접 묻지 않고, code-review가 요청 타당성을 판단한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/independent-control-plane/PHASE.md`
- `agent-roadmap/phase/independent-control-plane/milestones/control-plane-separation-migration.md`
- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-test/local/core-smoke.md`
- `services/core/go.mod`
- `services/core/cmd/oto-core/main.go`
- `services/core/internal/httpserver/server.go`
- `services/core/internal/httpserver/server_test.go`
- `services/core/internal/runnerregistry/registry.go`
- `services/core/internal/runnerregistry/registry_test.go`
- `proto/oto/runner.proto`
- `apps/runner/lib/oto/core/build_result.dart`
- `apps/runner/lib/oto/core/execution_context.dart`
- `apps/runner/lib/oto/application.dart`
- `apps/runner/test/oto_core_test.dart`
- `apps/runner/test/oto_application_test.dart`
- `Makefile`

### 테스트 환경 규칙

`test_env=local`이다. `agent-test/local/rules.md`를 읽었고, core/agent 관련 profile로 `agent-test/local/core-smoke.md`, `agent-test/local/agent-smoke.md`를 읽었다. 이 하위 계획은 Go core service 변경이 중심이라 최종 검증은 repository manifest의 `cd services/core && go test -count=1 ./...`를 사용한다. Dart 실행 결과 계약을 직접 바꾸지 않으므로 agent-smoke Dart 명령은 후속 runner/smoke 계획에서 다룬다.

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

- 서버 job 생성/조회/상태 전이: 기존 테스트 없음. `services/core/internal/cicdstate/*_test.go`와 `server_test.go`에 새 테스트가 필요하다.
- execution history, log append/list, artifact append/list: 기존 테스트 없음. in-memory store unit test와 HTTP handler test가 필요하다.
- iop 의존 배제: 기존 server/core 테스트는 iop를 참조하지 않는다. 새 API도 `services/core` 내부와 `proto/oto`만 사용하도록 검증한다.

### 심볼 참조

이 계획은 기존 symbol rename/remove를 요구하지 않는다. 새 package, 새 handler, 새 DTO만 추가한다.

### 분할 판단

split 정책을 먼저 평가했다. 이 work는 서버 상태/API, runner dispatch 계약, 통합 smoke/evidence가 서로 다른 검증과 위험을 가지므로 split이 필요하다. 공유 task group은 `m-control-plane-separation-migration`이다.

- `05+04_core_cicd_state`: archived predecessor `agent-task/archive/2026/06/m-control-plane-separation-migration/04+02_bootstrap_command/complete.log`로 04 완료 확인됨. 서버 상태/API foundation.
- `06+05_runner_dispatch_contract`: 05 완료 후 runner가 서버 job 계약을 소비하도록 확장.
- `07+05,06_cicd_smoke_evidence`: 05와 06 완료 후 통합 smoke 및 roadmap completion evidence.

### 범위 결정 근거

이 계획은 OTO Server의 in-memory 최소 상태 소유권만 구현한다. 실제 scheduling policy, durable DB/storage, artifact binary upload, auth/audit, runner가 job을 실행하는 client loop는 제외한다. 이 범위는 현재 Milestone의 범위 제외와 일치한다.

### 빌드 등급

`local-G06`. Go 서버 내부 상태 모델과 HTTP handler는 범위가 명확하고 `go test -count=1 ./...`로 검증 가능하다. storage/auth/protocol 장기 위험은 제외되어 local review로 충분하다.

## 구현 체크리스트

- [ ] `services/core/internal/cicdstate` in-memory store를 추가하고 job/execution/log/artifact 모델과 상태 전이를 구현한다.
- [ ] `services/core/internal/httpserver/server.go`에 job/execution/log/artifact JSON API를 추가한다.
- [ ] Go unit/handler 테스트로 생성, 조회, 상태 전이, 로그/artifact 누적, unknown id 오류를 검증한다.
- [ ] `cd services/core && go test -count=1 ./...`를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [CICD_STATE-1] 서버 CI/CD 상태 저장소

### 문제

[registry.go](/config/workspace/oto/services/core/internal/runnerregistry/registry.go:36)는 runner 상태만 저장한다. job/execution/log/artifact 상태를 둘 곳이 없어 OTO Server가 CI/CD 상태를 소유한다는 Task를 충족할 수 없다.

Before:

```go
36 type Registry struct {
37     mu      sync.RWMutex
38     now     func() time.Time
39     runners map[string]RunnerRecord
40 }
```

### 해결 방법

`services/core/internal/cicdstate/store.go`를 새로 만들고 `Store`에 jobs, executions, logs, artifacts map을 둔다. 최소 상태는 `queued`, `running`, `succeeded`, `failed`, `canceled` 문자열 상수로 둔다. 모든 snapshot/list는 slice/map copy를 반환한다.

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

- [ ] `services/core/internal/cicdstate/store.go`: 모델, 상태 상수, create/update/append/list/snapshot API.
- [ ] `services/core/internal/cicdstate/store_test.go`: copy 방어, 상태 전이, 누적 이벤트 테스트.

### 테스트 작성

작성한다. `TestStoreCreatesJobAndExecution`, `TestStoreAppendsLogsAndArtifacts`, `TestStoreRejectsUnknownExecution`을 추가한다.

### 중간 검증

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

기대 결과: package tests PASS.

## [CICD_STATE-2] HTTP API 표면

### 문제

[server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:29)는 registry만 주입하고 runner endpoint만 등록한다. CI/CD job/execution 상태를 생성하거나 조회하는 endpoint가 없다.

Before:

```go
29 func NewServerWithRegistry(addr string, registry *runnerregistry.Registry) *Server {
30     mux := http.NewServeMux()
31
32     mux.HandleFunc("/healthz", handleHealthz)
33     mux.HandleFunc("/readyz", handleReadyz)
34     mux.HandleFunc("/api/v1/runners/register", handleRunnerRegister(registry))
```

### 해결 방법

`NewServerWithRegistry`는 새 `cicdstate.Store`를 함께 만들고 내부 helper `NewServerWithRegistryAndStore`로 테스트 주입을 허용한다. Endpoint는 최소 JSON API로 둔다.

- `POST /api/v1/jobs`
- `GET /api/v1/jobs/{id}`
- `POST /api/v1/jobs/{id}/executions`
- `GET /api/v1/executions/{id}`
- `POST /api/v1/executions/{id}/logs`
- `GET /api/v1/executions/{id}/logs`
- `POST /api/v1/executions/{id}/artifacts`
- `GET /api/v1/executions/{id}/artifacts`

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

- [ ] `services/core/internal/httpserver/server.go`: store 주입, handlers, JSON writer helper.
- [ ] `services/core/internal/httpserver/server_test.go`: endpoint routing과 happy/error path.

### 테스트 작성

작성한다. `TestHandleCreateJob`, `TestHandleCreateExecution`, `TestHandleExecutionLogsAndArtifacts`, `TestHandleCicdUnknownIds`를 추가한다.

### 중간 검증

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

기대 결과: package tests PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/cicdstate/store.go` | CICD_STATE-1 |
| `services/core/internal/cicdstate/store_test.go` | CICD_STATE-1 |
| `services/core/internal/httpserver/server.go` | CICD_STATE-2 |
| `services/core/internal/httpserver/server_test.go` | CICD_STATE-2 |

## 최종 검증

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

기대 결과: 모든 Go 테스트 PASS. Go test cache output은 허용하지 않으므로 `-count=1`을 유지한다.

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