<!-- task=control_plane_phase_exit_followups/03_bootstrap_script_distribution plan=1 tag=REVIEW_BOOTSTRAP_SCRIPT_DISTRIBUTION_BOUNDARY -->

# Follow-up Plan - Bootstrap Script Drift Check

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

이 계획은 이전 리뷰의 Required 항목만 처리한다. 구현을 마치면 active `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운 뒤 active 파일을 그대로 두고 리뷰를 요청한다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret/service 준비, 또는 계획 충돌이 없으면 사용자에게 직접 질문하지 않는다. 그런 blocker가 생기면 `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 기록하고 중단한다. `USER_REVIEW.md`, log archive, `complete.log` 작성은 코드리뷰 에이전트 전용이다.

## 배경

첫 리뷰에서 provider 주입과 Go embed 방향은 확인되었지만, `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 실제 runner asset과 embedded script를 비교하지 않아 drift를 막지 못했다. 현재 두 파일은 동일하지만, 테스트가 동등성을 강제하지 않으면 runner asset과 core embedded copy가 조용히 갈라질 수 있다. 이번 후속은 drift 비교 테스트와 구현 증거 기록만 보강한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker가 생기면 active review stub의 `사용자 리뷰 요청` 섹션을 사용한다. 직접 사용자 프롬프트, 채팅 선택지 제시, `request_user_input` 호출은 금지된다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니라 `검증 결과`에 기록한다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/independent-control-plane/PHASE.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-test/local/core-smoke.md`
- `agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/plan_local_G05_0.log`
- `agent-task/control_plane_phase_exit_followups/03_bootstrap_script_distribution/code_review_local_G05_0.log`
- `services/core/internal/httpserver/bootstrap_provider.go`
- `services/core/internal/httpserver/routes.go`
- `services/core/internal/httpserver/runner_handlers.go`
- `services/core/internal/httpserver/server_test.go`
- `services/core/internal/httpserver/oto_agent_bootstrap.sh`
- `apps/runner/assets/script/shell/oto_agent_bootstrap.sh`

### 테스트 환경 규칙

- test_env: `local`
- `agent-test/local/rules.md`를 읽었다.
- `services/core/internal/httpserver/**`와 bootstrap script 경계 변경이므로 `agent-test/local/agent-smoke.md`를 적용한다.
- 적용 명령: `cd services/core && go test ./...`; 이번 계획에서는 캐시 회피를 위해 `cd services/core && go test -count=1 ./...`를 사용한다.
- `core-smoke`는 이전 계획에 기록되어 있어 확인했지만, 이번 변경의 직접 라우팅은 `agent-smoke`다.

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

- Provider-backed 성공 응답, method not allowed, provider error는 기존 구현 테스트가 다룬다.
- Embedded script non-empty는 다루지만, `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 runner asset과 embedded bytes를 직접 비교하지 않아 drift를 잡지 못한다.

### 심볼 참조

- 이름 변경/삭제 없음.
- `handleServeBootstrapScript` 호출부는 `routes.go`의 embedded provider 주입과 `server_test.go`의 static provider 주입으로 확인했다.

### 분할 판단

- 단일 plan으로 진행한다.
- 변경 대상은 `server_test.go`의 drift check 테스트 한 곳과 필요한 import 보강에 한정된다.
- API/foundation 변경, call-site rollout, 독립 검증 단위 분리, 상이한 위험 프로필이 없어서 split이 오히려 불필요한 coordination을 만든다.

### 범위 결정 근거

- `oto_agent_bootstrap.sh`의 내용, bootstrap command semantics, route path, provider interface, embed source 구조는 변경하지 않는다.
- drift 비교 테스트가 필요로 하는 최소 import와 assertion만 수정한다.
- 새 Makefile target은 만들지 않는다. Go test 안에서 runner asset과 embedded bytes를 직접 비교하는 방식으로 이전 계획의 drift check 요구를 만족시킨다.

### 빌드 등급

- `local-G06`: 실패 원인이 명확하고 deterministic한 테스트 보강이지만, monorepo 상대 경로와 verification trust를 바로잡아야 하므로 이전 `G05`보다 한 단계 올린다.

## 구현 체크리스트

- [ ] `services/core/internal/httpserver/server_test.go`의 `TestEmbeddedBootstrapScriptMatchesRunnerAsset`가 `apps/runner/assets/script/shell/oto_agent_bootstrap.sh`와 `embeddedBootstrapProvider{}.bootstrapScript()` 결과를 직접 비교하도록 보강한다.
- [ ] `cd services/core && go test -count=1 ./...`를 실행하고 실제 출력 요약을 `CODE_REVIEW-local-G06.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_BOOTSTRAP_SCRIPT_DISTRIBUTION_BOUNDARY-1] 실제 drift 비교 테스트 추가

#### 문제

`services/core/internal/httpserver/server_test.go:536`의 `TestEmbeddedBootstrapScriptMatchesRunnerAsset`는 runner asset과 embedded copy의 drift check라고 설명하지만, 실제 assertion은 embedded content non-empty뿐이다. 현재 `cmp -s apps/runner/assets/script/shell/oto_agent_bootstrap.sh services/core/internal/httpserver/oto_agent_bootstrap.sh`는 통과하지만, Go test가 이 동등성을 보장하지 않는다.

#### 해결

`TestEmbeddedBootstrapScriptMatchesRunnerAsset`에서 runner asset 파일을 읽고 embedded provider bytes와 직접 비교한다. `go test` 실행 cwd가 `services/core/internal/httpserver` package 디렉터리임을 기준으로 runner asset 상대 경로는 `../../../../apps/runner/assets/script/shell/oto_agent_bootstrap.sh`를 사용한다.

비교 실패 시에는 core embedded copy를 runner asset에서 다시 동기화하라는 메시지를 남긴다. 별도 non-empty 테스트는 유지해도 되지만, 동등 비교 테스트만으로 non-empty가 충분히 검증되면 중복을 줄여도 된다.

#### 체크리스트

- [ ] 필요한 경우 `os` 또는 표준 라이브러리 import를 추가한다.
- [ ] runner asset read 실패를 `t.Fatalf`로 보고한다.
- [ ] `bytes.Equal` 또는 동등한 byte 비교로 embedded bytes와 runner asset bytes를 비교한다.
- [ ] 실패 메시지에 동기화 대상 파일 경로를 포함한다.

#### 테스트 결정

`cd services/core && go test -count=1 ./...`를 필수로 실행한다. 이 명령이 drift check 테스트를 포함해야 한다.

## 수정 파일 요약

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

## 최종 검증

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

예상 결과: 모든 services/core package 테스트가 통과하고, `services/core/internal/httpserver` package에 drift 비교 테스트가 포함된다.

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