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

# Plan - REVIEW_API3

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

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

## 배경

세 번째 리뷰에서 structured variables merge와 실제 치환 assertion은 충족됐지만, remote variables가 있을 때 원래 `Application.build()`가 거부해야 할 invalid `property` 타입을 정상 map으로 바꿔 validation을 우회하는 문제가 발견됐다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 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_cloud_G07_2.log`
- Archived review: `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/code_review_cloud_G07_2.log`
- Verdict: FAIL

## 범위 결정 근거

- 서버 next-claim, runner shutdown, failure report, structured merge, 실제 치환 assertion은 다시 넓히지 않는다.
- invalid `property` 타입 validation 보존과 그 테스트만 수정한다.

## 빌드 등급

- build: `cloud-G07`. 반복된 review loop이므로 같은 lane에서 검증 신뢰를 닫는다.
- review: `cloud-G07`. validation bypass 여부를 다시 확인한다.

## 구현 체크리스트

- [ ] `root['property']`가 존재하지만 Map이 아니면 variables merge가 이를 정상 map으로 덮어쓰지 않는다.
- [ ] invalid `property` 타입이 remote variables와 함께 들어와도 `Application.build()` 또는 `RemoteRunExecutor.runOnce()`가 failure로 처리하고 execution report를 전송한다.
- [ ] 기존 block-style, no-property, inline-map variables merge 테스트는 계속 통과한다.
- [ ] Dart 검증과 CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 상세 구현 항목

### [REVIEW_API3-1] Preserve Invalid Property Validation

문제: `apps/runner/lib/oto/agent/remote_run_executor.dart:190`이 빈 `mergedProperty`를 만들고 기존 `property`가 `Map`일 때만 복사한 뒤, `remote_run_executor.dart:202`에서 항상 `root['property'] = mergedProperty`로 덮어쓴다. `property: [bad]` 같은 invalid YAML은 remote variables가 있으면 정상 map으로 바뀌어 `Application._validateBuildMap()`의 property 타입 검증을 우회한다.

해결 방법: `root`에 `property`가 있고 그 값이 `Map`이 아니면 merge하지 않는다. 원본 YAML을 그대로 반환해 `Application.build()`가 기존 validation failure를 만들게 하거나, 동일한 의미의 `BuildResult.failure`로 report되게 한다.

체크리스트:
- [ ] `mergeVariablesIntoYaml()` 단위 테스트에서 invalid property list가 원본 YAML을 유지하거나 failure 경로로 이어짐을 검증한다.
- [ ] `runOnce()` smoke 또는 fake client 테스트에서 invalid property + remote variables가 success가 아니라 failure report로 이어짐을 검증한다.
- [ ] 기존 valid property variants 테스트는 유지한다.

중간 검증:

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/runner/lib/oto/agent/remote_run_executor.dart` | REVIEW_API3-1 |
| `apps/runner/test/oto_agent_registration_test.dart` | REVIEW_API3-1 |
| `apps/runner/test/oto_server_connection_smoke_test.dart` | REVIEW_API3-1 |

## 최종 검증

```bash
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으로 종료한다.
