<!-- task=m-remote-run-lifecycle/01_run_request_pipeline plan=0 tag=API -->

# Plan - API

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

구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다. 재실행으로 채울 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 OTO Server는 job을 만들고 runner가 claim할 수 있지만, job에 원격 실행 입력이 저장되지 않는다. `proto/oto/runner.proto`에는 `RunRequest`와 `JobClaimResponse.run_request`가 이미 있으나 서버 claim 응답은 이를 채우지 않는다. 이 계획은 run request를 서버 job 상태에 저장하고 runner가 claim할 때 기존 YAML 파이프라인 입력으로 받을 수 있는 계약을 만든다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/agent/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/pipeline/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/pipeline-smoke.md`
- `agent-test/local/command-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/message-based-build-agent/PHASE.md`
- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
- `.gitignore`
- `Makefile`
- `proto/oto/runner.proto`
- `services/core/go.mod`
- `services/core/internal/cicdstate/store.go`
- `services/core/internal/cicdstate/store_test.go`
- `services/core/internal/httpserver/server.go`
- `services/core/internal/httpserver/server_test.go`
- `services/core/internal/runnerregistry/registry.go`
- `apps/runner/pubspec.yaml`
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
- `apps/runner/lib/oto/agent/agent_runner.dart`
- `apps/runner/lib/oto/application.dart`
- `apps/runner/lib/oto/core/build_result.dart`
- `apps/runner/lib/oto/core/data_composer.dart`
- `apps/runner/lib/oto/core/execution_context.dart`
- `apps/runner/lib/oto/pipeline/pipeline.dart`
- `apps/runner/lib/cli/commands/command_exe.dart`
- `apps/runner/test/oto_agent_registration_test.dart`
- `apps/runner/test/oto_server_connection_smoke_test.dart`
- `apps/runner/test/oto_application_test.dart`
- `apps/runner/test/oto_context_test.dart`
- `apps/runner/test/oto_core_test.dart`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md`를 읽었다.
- 적용 프로파일: `agent-smoke`, `core-smoke`, `pipeline-smoke`, `command-smoke`.
- agent/core/pipeline/command 프로파일의 Dart 기준은 `dart analyze`와 관련 `dart test`다.
- `services/core/**`와 `proto/oto/**`에 대한 agent-test 프로파일은 없다. 이 계획은 `Makefile`의 `core-test`와 `proto-go`/`proto-dart` 타깃을 fallback 검증 근거로 쓴다.
- `<확인 필요>` 값은 없었다.
- 사전 검증으로 `go test ./...`, `dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart`, `dart test test/oto_server_connection_smoke_test.dart`가 통과했다.

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

- 서버 job 생성은 `id/name`만 저장하며 run request 저장 테스트가 없다.
- runner claim 응답은 `RunRequest` 필드를 프로토콜에 노출하지만 서버 응답에 채우는 테스트가 없다.
- queue claim 또는 서버 소유 execution id 선택 기준이 없다. 기존 client는 명시 `jobId/executionId` claim만 검증한다.
- YAML 입력 변환 자체는 `Application.build(BuildType.file, yamlContent: ...)` 테스트가 커버하지만, 서버 claim 응답과 연결되어 있지 않다.

### 심볼 참조

- renamed/removed symbol: none.
- 변경 후보 call site: `Store.CreateJob` 호출은 `services/core/internal/httpserver/server.go`, `services/core/internal/httpserver/server_test.go`, `services/core/internal/cicdstate/store_test.go`에서 확인했다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 task group: `m-remote-run-lifecycle`.
- `01_run_request_pipeline`: 선행 없음. 서버 job/run request 저장과 claim 응답 계약을 먼저 만든다.
- `02+01_step_log_stream`: `01`의 active/archived `complete.log`가 아직 없으므로 predecessor `01`은 missing이다.
- `03+01_artifact_event`: predecessor `01`은 missing이다.
- `04+01_cancel_status_update`: predecessor `01`은 missing이다.
- 서버 계약이 없으면 runner loop, artifact, cancel/status/self-update 구현이 추측으로 흐르므로 단일 plan보다 split이 안전하다.

### 범위 결정 근거

- 이 계획은 서버 job/run request 저장, claim 응답, Dart job client의 claim result 파싱까지만 다룬다.
- 이 plan은 후속 구현을 가능하게 하는 선행 계약이며, PASS되어도 roadmap Task를 체크하지 않는다.
- runner가 claim된 request를 실제 실행하는 loop는 `02+01_step_log_stream`으로 분리한다.
- artifact 자동 수집은 `03+01_artifact_event`로 분리한다.
- cancel/status/self-update endpoint와 상태 전이는 `04+01_cancel_status_update`로 분리한다.
- 새 protobuf 메시지는 만들지 않는다. 기존 `RunRequest`와 `JobClaimResponse.run_request`를 사용한다.

### 빌드 등급

- build: `cloud-G07`. Go service 상태 저장, Dart client 파싱, cross-language protocol JSON 계약을 함께 바꾸는 작업이다.
- review: `cloud-G07`. server/client/test가 같은 계약을 구현했는지 교차 확인해야 한다.

## 구현 체크리스트

- [ ] 서버 Store가 run request 입력을 job에 저장하고 copy/snapshot에서 누락하지 않는다.
- [ ] `POST /api/v1/jobs`가 `run_request`를 받아 검증하고, `GET /jobs/{id}`가 저장된 입력을 노출한다.
- [ ] runner claim 응답이 저장된 `RunRequest`를 채우며, missing/invalid run input은 명확한 오류로 거부한다.
- [ ] Dart `JobClaimResult`가 `runRequest`를 파싱하고 기존 명시 claim 테스트를 깨지 않는다.
- [ ] Go unit/smoke와 Dart agent/server smoke 테스트를 갱신한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Store Run Input

문제: [services/core/internal/cicdstate/store.go](/config/workspace/oto/services/core/internal/cicdstate/store.go:25)의 `Job`은 execution id만 저장하고 run input을 저장하지 않는다.

Before:

```go
// services/core/internal/cicdstate/store.go:25
type Job struct {
	ID          string
	Name        string
	State       string
	CreatedAt   time.Time
	UpdatedAt   time.Time
	ExecutionID string
}
```

해결 방법: `cicdstate.RunInput`을 추가하고 `Job`에 `RunInput *RunInput`을 둔다. proto package를 store에 직접 끌어오지 말고 HTTP boundary에서 `otopb.RunRequest` <-> `cicdstate.RunInput` 변환을 수행한다. `CreateJob`은 run input pointer를 optional로 받되 copy해서 저장한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/cicdstate/store.go`: `RunInput` 구조체, deep copy helper, `Job.RunInput`, `CreateJob` signature 추가.
- [ ] `services/core/internal/cicdstate/store_test.go`: 저장/복사/변경 누수 방지 테스트 추가.

테스트 작성: `TestStoreCreatesJobWithRunInputCopy`를 추가한다. variables/commandTypes slice/map을 외부에서 바꿔도 store 값이 바뀌지 않아야 한다.

중간 검증:

```bash
go test ./internal/cicdstate
```

### [API-2] HTTP Job Create And Claim Contract

문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:485)의 job create request는 `id/name`만 받고, [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:776)의 claim response는 `RunRequest`를 비워 둔다.

Before:

```go
// services/core/internal/httpserver/server.go:487
var req struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}
```

```go
// services/core/internal/httpserver/server.go:776
writeResponse(w, http.StatusOK, &otopb.JobClaimResponse{
	Accepted:    true,
	RunnerId:    runnerID,
	JobId:       jobID,
	ExecutionId: execID,
	State:       cicdstate.StateRunning,
})
```

해결 방법: create body에 `run_request`를 추가한다. `pipeline_yaml`와 `pipeline_yaml_path` 중 정확히 하나를 remote claim 기준으로 인정하고, `variables`와 `command_types`는 optional로 보존한다. claim 시 저장된 input이 없거나 둘 다/둘 다 없음이면 `400` 또는 `409`로 거부하고, 응답에는 runner/job/execution id를 채운 `RunRequest`를 포함한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/httpserver/server.go`: `runRequestFromJSON`, `runRequestToProto`, `runInputToJSON` helper 추가.
- [ ] `services/core/internal/httpserver/server.go`: `handleCreateJob`, `jobToJSON`, `handleRunnerClaimJob` 갱신.
- [ ] `services/core/internal/httpserver/server_test.go`: create/get/claim run_request 테스트와 invalid input 테스트 추가.

테스트 작성: `TestHandleCreateJobStoresRunRequest`, `TestHandleRunnerCicdClaimReturnsRunRequest`, `TestHandleRunnerCicdRejectsInvalidRunInput`를 추가한다.

중간 검증:

```bash
go test ./internal/httpserver
```

### [API-3] Dart Claim Result Parses RunRequest

문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:9)의 `JobClaimResult`는 `run_request`를 보존하지 않는다. 이후 runner loop가 서버 입력을 실행할 수 없다.

Before:

```dart
// apps/runner/lib/oto/agent/oto_server_job_client.dart:9
class JobClaimResult {
  final bool accepted;
  final String? errorMessage;
  final String jobId;
  final String executionId;
  final String state;
```

해결 방법: `JobClaimResult`에 `oto.RunRequest? runRequest`를 추가하고 snake_case/camelCase JSON 모두 파싱한다. `claimJob`의 기존 API는 유지하되, 후속 `02`가 쓸 `claimNextJob()` 추가 여지를 남긴다.

수정 파일 및 체크리스트:

- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: run request JSON parser helper 추가.
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: claim response에 `run_request`가 있을 때 Dart 객체에 보존되는 테스트 추가.
- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 서버 create job body에 inline YAML `run_request`를 넣고 claim 결과를 확인한다.

테스트 작성: 기존 `job client claims jobs and reports build results` 테스트를 확장한다.

중간 검증:

```bash
dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/cicdstate/store.go` | API-1 |
| `services/core/internal/cicdstate/store_test.go` | API-1 |
| `services/core/internal/httpserver/server.go` | API-2 |
| `services/core/internal/httpserver/server_test.go` | API-2 |
| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-3 |
| `apps/runner/test/oto_agent_registration_test.dart` | API-3 |
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-3 |

## 최종 검증

```bash
cd services/core && go test ./...
cd apps/runner && dart analyze
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart
```

예상 결과: 모든 명령이 exit code 0으로 종료한다. Go test cache 출력은 이 계획에서 허용한다. Dart test는 실제 stdout/stderr를 review stub에 붙인다.

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