<!-- task=m-remote-run-lifecycle/02+01_step_log_stream plan=1 tag=REVIEW_API -->

# Plan - REVIEW_API

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

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

## 배경

첫 리뷰에서 remote runner의 claim loop, shutdown, variables merge, path confinement, failure report 계약이 구현되지 않았거나 테스트가 해당 계약을 검증하지 않는 것으로 확인됐다. 이 follow-up은 원격 실행 생명주기의 `run-input`과 `step-log` 완료 조건을 다시 충족시키기 위한 보완 작업이다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
- Task ids:
  - `run-input`: run request가 pipeline 실행 입력으로 변환되는 기준이 정의되어 있다.
  - `step-log`: step event와 log stream이 원격 소비자에게 전달되는 흐름이 정의되어 있다.
- Completion mode: check-on-pass

## 분석 결과

### 이전 루프

- Archived plan: `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/plan_local_G07_0.log`
- Archived review: `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/code_review_cloud_G06_0.log`
- Verdict: FAIL

### 범위 결정 근거

- 핵심 실패는 사용자 환경이 아니라 repository-owned API/runner 계약 문제다.
- `claimNextJob()`가 서버의 현재 `/jobs/claim` 계약과 맞지 않으므로, Dart-only 수정으로 단정하지 말고 Go server 계약과 Dart client loop를 함께 맞춘다.
- artifact 자동 보고, cancel/status/self-update는 sibling subtasks의 범위이므로 건드리지 않는다.

### 빌드 등급

- build: `cloud-G07`. Go server claim 계약, Dart long-running runner loop, signal/shutdown, remote execution report 신뢰성을 함께 다룬다.
- review: `cloud-G07`. protocol/server 계약과 runner loop 검증을 다시 확인해야 한다.

## 구현 체크리스트

- [ ] 서버와 Dart runner 사이의 next-job claim 계약을 실제 동작 가능한 형태로 맞춘다.
- [ ] default runner loop가 shutdown 요청을 받으면 polling delay 중에도 빠져나와 session close까지 진행한다.
- [ ] `RunRequest.variables`가 parsed YAML `property`에 병합되어 pipeline tag에서 실제로 읽힌다.
- [ ] `pipelineYamlPath` workspace confinement가 canonical root 경계 기준으로 sibling-prefix escape를 거부한다.
- [ ] parse/file-read/build 실패도 `BuildResult.failure`로 execution report가 전송되도록 `runOnce()` 실패 경로를 감싼다.
- [ ] Dart/Go 검증과 CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 상세 구현 항목

### [REVIEW_API-1] Next Job Claim Contract

문제: `apps/runner/lib/oto/agent/oto_server_job_client.dart:114`의 `claimNextJob()`가 빈 `job_id`/`execution_id`로 `/jobs/claim`을 호출하지만, `services/core/internal/httpserver/server.go:722`는 두 값을 필수로 요구한다.

해결 방법: 서버와 Dart client가 공유하는 next-job claim 경로를 구현한다. 선택지는 서버가 runner별 queued job을 선택하고 execution id를 생성하거나, Dart runner가 합법적으로 claim할 job id와 execution id를 얻는 endpoint를 추가하는 것이다. 기존 explicit `claimJob(jobId, executionId)` 테스트는 유지한다.

체크리스트:
- [ ] Go server handler/test에서 no-job과 queued-job next claim의 응답 계약을 정의한다.
- [ ] Dart `OtoServerJobClient.claimNextJob()`가 그 계약을 사용하고 HTTP 400/500을 no-job으로 숨기지 않는다.
- [ ] default runner loop가 queued job을 claim해서 `RemoteRunExecutor.runOnce()`에 전달하는 테스트를 추가한다.

중간 검증:

```bash
cd services/core && go test ./internal/httpserver
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
```

### [REVIEW_API-2] Shutdown-Aware Default Loop

문제: `apps/runner/lib/oto/agent/agent_runner.dart:91`의 `shutdown` completer가 `while (true)`와 연결되지 않아 SIGINT/SIGTERM 이후에도 loop가 계속 돈다. 또한 `_defaultJobLoop()`가 끝나지 않으면 `run()`의 `_awaitShutdown()`은 실행되지 않는다.

해결 방법: default loop가 shutdown future를 인자로 받거나 내부 signal future와 polling delay를 `Future.any`로 race한다. 테스트에서는 signal 대신 주입 가능한 shutdown을 사용해 no-job polling 중에도 종료되는지 검증한다.

체크리스트:
- [ ] `_defaultJobLoop()`가 shutdown 완료 시 loop를 종료한다.
- [ ] `run()`에서 default loop와 shutdown wait의 책임이 중복되지 않도록 정리한다.
- [ ] no-job 상태에서 shutdown이 들어오면 session이 닫히는 테스트를 추가한다.

중간 검증:

```bash
cd apps/runner && dart test test/oto_agent_registration_test.dart
```

### [REVIEW_API-3] Variables Merge Into Pipeline Property

문제: `apps/runner/lib/oto/agent/remote_run_executor.dart:113`에서 singleton property에 variables를 넣어도 `Application.build()`가 `apps/runner/lib/oto/application.dart:128`에서 YAML property로 덮어써 값이 사라진다.

해결 방법: `RunRequestParser` 또는 executor가 YAML을 structured map으로 파싱해 `property` map에 variables를 병합한 뒤 다시 build 입력으로 넘긴다. 기존 YAML property가 있으면 remote variables가 override하는 기준을 명시하고 테스트한다.

체크리스트:
- [ ] variables 병합이 `Application.build()` 이전의 YAML 입력에 반영된다.
- [ ] 기존 `property.workspace` 기본값 흐름을 깨지 않는다.
- [ ] `<!property.FLAVOR>`를 사용하는 실제 Print pipeline 실행 테스트를 추가한다.

중간 검증:

```bash
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart
```

### [REVIEW_API-4] Workspace Path Confinement Boundary

문제: `apps/runner/lib/oto/agent/remote_run_executor.dart:58`의 `startsWith(workspaceRoot)` 검사는 `/var/workspace2` 같은 sibling-prefix escape를 허용할 수 있다.

해결 방법: workspace root와 대상 경로를 모두 canonicalize하고, `path.equals(root, target) || path.isWithin(root, target)`로 경계를 확인한다. path가 존재하지 않는 경우에도 parent 기준 escape를 안전하게 거부한다.

체크리스트:
- [ ] canonical root 경계 검사를 사용한다.
- [ ] `../` traversal, absolute sibling prefix, valid child path를 모두 테스트한다.
- [ ] 파일 누락은 confinement 통과 후 실행 실패로 report되는 경로와 연결한다.

중간 검증:

```bash
cd apps/runner && dart test test/oto_agent_registration_test.dart
```

### [REVIEW_API-5] Report Failures From Parse/File/Build

문제: `RemoteRunExecutor.runOnce()`는 `_parser.parse()`와 file read가 report try/catch 밖에 있어, parser/file failure 시 서버 execution report가 전송되지 않는다.

해결 방법: runOnce 전체를 try/catch로 감싸고, parse/file/build 실패를 `BuildResult.failure(error, stackTrace, exitCode: 10)`로 만든 뒤 `reportExecution()`을 항상 시도한다. report 자체 실패는 기존처럼 log만 남기되 반환 result는 failure로 유지한다.

체크리스트:
- [ ] parse/file-read failure가 execution report payload로 전송된다.
- [ ] `Application.build()`가 반환한 failure도 report된다.
- [ ] 실패 report가 서버 execution/job state를 failed로 전환하는 smoke를 추가한다.

중간 검증:

```bash
cd apps/runner && dart test test/oto_server_connection_smoke_test.dart
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/httpserver/server.go` | REVIEW_API-1 |
| `services/core/internal/httpserver/server_test.go` | REVIEW_API-1 |
| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | REVIEW_API-1 |
| `apps/runner/lib/oto/agent/agent_runner.dart` | REVIEW_API-1, REVIEW_API-2 |
| `apps/runner/lib/oto/agent/remote_run_executor.dart` | REVIEW_API-3, REVIEW_API-4, REVIEW_API-5 |
| `apps/runner/test/oto_agent_registration_test.dart` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-3, REVIEW_API-4 |
| `apps/runner/test/oto_server_connection_smoke_test.dart` | REVIEW_API-1, REVIEW_API-5 |

## 최종 검증

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

예상 결과: 모든 명령이 exit code 0으로 종료한다. 만약 Go server claim 계약을 건드린 범위가 `./internal/httpserver`를 넘어가면 `cd services/core && go test ./...`를 추가로 실행하고 실제 출력도 review stub에 기록한다.
