<!-- task=m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply plan=0 tag=REFRESH_APPLY -->

# Plan - REFRESH_APPLY

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

이 계획은 dependent split task다. active 또는 archived `02_refresh_contract/complete.log`가 확인되기 전에는 구현을 시작하지 않는다. 구현 후에는 반드시 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. review finalization, 로그 rename, `complete.log` 작성, archive 이동은 code-review 스킬 전용이다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 review stub의 `사용자 리뷰 요청` 섹션을 정확한 연결 근거와 함께 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나, chat 선택지를 제시하거나, `request_user_input`을 호출하거나, `USER_REVIEW.md`를 만들지 않는다. 환경/secret/service 준비, 일반 범위 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 계획으로 기록한다.

## 배경

`02_refresh_contract`는 running process entrypoint와 diff classification을 세운다. 이 plan은 그 계약 위에서 apply가 실패해도 기존 `/v1/models`, provider-pool dispatch, queue state가 손상되지 않는 atomic apply 경계를 만든다. 또한 operator가 확인할 수 있도록 refresh 결과에 changed node/provider/model 항목을 남긴다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone 잠금 결정이 실구현을 막을 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 사용하며, 직접 사용자 prompt는 금지된다. code-review가 요청의 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `atomic-apply`: validation 또는 apply 실패 시 기존 routing/runtime 상태가 유지된다.
  - `ops-report`: refresh 결과가 CLI/API 응답, 로그, event 중 최소 한 표면에 남고 변경된 node/provider/model 항목을 확인할 수 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.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/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/PHASE.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.md`
- `apps/edge/internal/edgecmd/config.go`
- `apps/edge/internal/edgecmd/root.go`
- `apps/edge/internal/edgecmd/edgecmd_test.go`
- `apps/edge/cmd/edge/main.go`
- `apps/edge/internal/bootstrap/module.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/artifact_server.go`
- `apps/edge/internal/bootstrap/runtime_test.go`
- `apps/edge/internal/input/manager.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/ollama_passthrough.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/run_result.go`
- `apps/edge/internal/openai/strict_output.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/status_provider.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/node_command.go`
- `apps/edge/internal/service/control_command.go`
- `apps/edge/internal/service/service_internal_test.go`
- `apps/edge/internal/service/status_provider_test.go`
- `apps/edge/internal/service/run_dispatch_internal_test.go`
- `apps/edge/internal/service/service_test.go`
- `apps/edge/internal/service/model_queue_test.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/registry.go`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `proto/iop/runtime.proto`
- `configs/edge.yaml`

### SDD 기준

- SDD: `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`.
- 대상 Acceptance Scenario:
  - S10 -> `atomic-apply`: 기존 `/v1/models`와 provider routing이 정상일 때 일부 apply 실패 refresh 후에도 기존 상태가 유지되어야 한다.
  - S11 -> `ops-report`: refresh가 `applied`, `rejected`, `restart_required` 중 하나로 끝나면 changed node/provider/model 항목과 status가 CLI/API/log/event 중 최소 하나에 남아야 한다.
- Evidence Map:
  - S10: `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply`, atomic apply failure regression test.
  - S11: same task, CLI/API/log/event assertion for refresh report.
- 반영 방식: 구현 checklist는 apply staging/commit/rollback 경계, service/openai runtime snapshot 동기화, queue policy update, invalid refresh 후 `/v1/models`와 provider dispatch 유지, report surface 검증으로 구성한다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`가 존재해 전체를 읽었다.
- 매칭 profile:
  - `agent-test/local/edge-smoke.md`: Edge 단위 테스트 `go test ./apps/edge/...`; smoke/e2e는 field 환경 준비가 필요한 경우 별도 후속 evidence로 남긴다.
  - `agent-test/local/platform-common-smoke.md`: config/proto 공통 확인 `go test ./packages/go/... ./proto/gen/...`; proto 변경 없음이 원칙이다.
  - `agent-test/local/testing-smoke.md`: 테스트 규칙 자체는 수정하지 않는다.
- 적용 명령은 Go cache를 허용하지 않도록 `-count=1`을 사용한다. 실제 field capacity smoke는 refresh 에픽 이후 `field-refresh` task 범위라 이 plan의 PASS 조건에 넣지 않는다.

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

- Apply failure previous-state preservation: 기존 테스트 없음. 실패 주입 hook 또는 staged apply error로 기존 `/v1/models`와 dispatch 상태가 유지되는 regression test가 필요하다.
- Service state concurrency: `Service.SetNodeStore`와 `SetModelCatalog`는 단순 setter라 동시 refresh/read 보호 테스트가 없다.
- OpenAI model catalog refresh: 기존 `TestModelsUsesConfiguredModelsOrTarget`는 fallback/configured models만 검증하고 provider-pool catalog refresh 후 `/v1/models` 변화를 검증하지 않는다.
- Queue policy refresh: `modelQueueManager.getOrCreateGroupLocked`는 기존 group policy를 유지하므로 provider `max_queue`/`queue_timeout_ms` refresh test가 없다.
- Ops report: CLI/API response에 changed node/provider/model 항목이 남는지 검증하는 테스트가 없다.

### 심볼 참조

- renamed/removed symbol 없음.
- 02 plan에서 추가될 것으로 기대되는 `Runtime.RefreshConfig`, `configrefresh.Result`, `configrefresh.Change`를 확장 사용한다. 02 구현명이 다르면 이 plan 구현 전 review/plan 변경 사항에 실제 symbol mapping을 기록한다.

### 분할 판단

- split decision policy를 plan 파일 선택 전에 평가했다.
- 공유 task group: `agent-task/m-runtime-reconnect-config-refresh`.
- sibling plan:
  - `02_refresh_contract`: entrypoint와 diff classifier. 이 plan의 predecessor.
  - `03+02_edge_refresh_apply`: atomic apply와 ops report. directory name이 `+02`를 선언한다.
- predecessor `02` completion:
  - active lookup `agent-task/m-runtime-reconnect-config-refresh/02_*/complete.log`, `02+*/complete.log`: 없음.
  - archive lookup `agent-task/archive/*/*/m-runtime-reconnect-config-refresh/02_*/complete.log`, `02+*/complete.log`: 없음.
  - 현재 세션에서 `agent-task/m-runtime-reconnect-config-refresh/02_refresh_contract/PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 생성했지만 `complete.log`는 없다.
- 구현은 `02_refresh_contract` PASS 후 시작해야 한다. 02가 FAIL/WARN으로 follow-up plan을 만든 경우 그 follow-up까지 완료되어 active 또는 archived `02_*` complete.log가 생긴 뒤 진행한다.

### 범위 결정 근거

- Node config push/ack, generated proto, Node adapter diff/drain/runtime-config apply는 node-config 에픽이므로 제외한다.
- Field GX10/OneXPlayer capacity smoke와 docs 갱신은 field 에픽이므로 제외한다.
- 이번 plan은 Edge-local runtime state와 report surface만 다룬다. Control Plane command surface는 02 plan에서 선택한 MVP 표면이 아니면 추가하지 않는다.

### 빌드 등급

- `cloud-G08`: running Edge apply, shared runtime state, OpenAI route catalog, provider dispatch queue, rollback regression이 한꺼번에 걸린다. 02 contract에 의존하지만 실패 보존과 동시 read/write 안정성이 핵심이라 cloud 고난도 작업이다.

## 구현 체크리스트

- [ ] [REFRESH_APPLY-1] active 또는 archived `02_refresh_contract` complete.log 존재를 확인하고, refresh apply를 staged validation -> commit 순서의 atomic boundary로 구현한다.
- [ ] [REFRESH_APPLY-2] Service/OpenAI/NodeStore runtime snapshot update를 동기화하고 provider capacity/queue policy refresh가 후속 dispatch에 반영되게 한다.
- [ ] [REFRESH_APPLY-3] invalid provider refresh 또는 apply failure 주입 후 기존 `/v1/models`와 provider dispatch 상태가 유지되는 regression test를 작성한다.
- [ ] [REFRESH_APPLY-4] refresh API/CLI 응답에 changed node/provider/model 항목과 status를 남기고 ops-report assertion을 추가한다.
- [ ] [REFRESH_APPLY-5] `atomic-apply`와 `ops-report` 대상 최종 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

1. `find agent-task -path '*/m-runtime-reconnect-config-refresh/02_refresh_contract/complete.log' -print -quit`가 active 또는 archived complete.log 경로를 출력하는지 확인한다. 출력이 없으면 구현을 시작하지 않는다.
2. 02 완료 로그의 Roadmap Completion이 `entrypoint`, `diff-classify`를 PASS로 확인했는지 읽고, 실제 symbol/API 이름이 이 plan의 기대와 다르면 `계획 대비 변경 사항`에 mapping을 기록한다.
3. 이 plan은 directory name상 predecessor가 `02` 하나뿐이다. 다른 숨은 선행 의존성을 추가하지 않는다.

## REFRESH_APPLY-1 - Atomic Runtime Apply Boundary

### 문제

현재 `Runtime`은 시작 시 config snapshot을 조립한 뒤 갱신 경계가 없다. `Service`의 node store와 model catalog도 단순 setter라 apply 중간 실패를 되돌릴 단위가 없다.

Before:

```go
// apps/edge/internal/service/service.go:38
func (s *Service) SetNodeStore(store *edgenode.NodeStore) {
	s.nodeStore = store
	if s.queue != nil {
		s.queue.setStore(store)
	}
}

// apps/edge/internal/service/service.go:47
func (s *Service) SetModelCatalog(catalog []config.ModelCatalogEntry) {
	s.modelCatalog = catalog
}
```

Runtime 시작도 한 번만 주입한다.

```go
// apps/edge/internal/bootstrap/runtime.go:54
svc := edgeservice.New(registry, bus)
svc.SetNodeStore(nodeStore)
svc.SetModelCatalog(cfg.Models)
inputManager := edgeinput.NewManager(*cfg, svc, logger.Named("input"))
```

### 해결 방법

- `Runtime`에 `refreshMu`를 추가해 concurrent refresh apply를 직렬화한다.
- apply는 새 config load/validation/classify, 새 `NodeStore`/model catalog staging, report 생성, commit 순서로 진행한다.
- commit 전까지 기존 `Runtime.Cfg`, `Service` state, `OpenAI` catalog를 변경하지 않는다.
- commit은 한 함수에서 수행하고, error 반환 가능한 단계는 commit 전에 끝낸다. 테스트용 failure injection hook은 commit 전에만 둔다.
- rejected/restart_required 결과는 state를 변경하지 않는다.
- apply 성공 후 `Runtime.Cfg` snapshot, `Service` state, `Input.OpenAI` catalog를 함께 갱신한다.

After shape:

```go
// apps/edge/internal/bootstrap/runtime.go
func (r *Runtime) ApplyRefresh(ctx context.Context, req configrefresh.Request) (configrefresh.Result, error) {
	r.refreshMu.Lock()
	defer r.refreshMu.Unlock()

	staged, result, err := r.stageRefresh(ctx, req)
	if err != nil || result.Status != configrefresh.StatusApplied {
		return result, err
	}
	return r.commitRefresh(staged, result)
}
```

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

- [ ] `apps/edge/internal/bootstrap/runtime.go`: `refreshMu`, staged apply, commit helper 추가.
- [ ] `apps/edge/internal/bootstrap/refresh_admin.go`: apply mode가 atomic apply helper를 호출하게 조정.
- [ ] `apps/edge/internal/configrefresh/*.go`: result/report 구조가 staged apply에 필요한 changed item metadata를 담도록 확장.
- [ ] `apps/edge/internal/node/store.go`: 필요 시 immutable replacement/clone helper 추가.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: rejected/restart_required가 state를 바꾸지 않는 테스트 추가.

### 테스트 작성

- 작성: `apps/edge/internal/bootstrap/runtime_test.go`에 `TestRefreshApplyDoesNotMutateOnRejectedCandidate`, `TestRefreshApplyDoesNotMutateOnRestartRequired`.
- 작성: failure hook을 둔다면 `TestRefreshApplyFailurePreservesRuntimeSnapshot`.

### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/...
```

예상: refresh apply boundary와 기존 Edge package 테스트 PASS.

## REFRESH_APPLY-2 - Runtime Snapshot Synchronization

### 문제

OpenAI `/v1/models`와 route dispatch는 `modelCatalog`를 직접 읽고, provider-pool dispatch는 `Service.modelCatalog`와 `Service.nodeStore`를 직접 읽는다. refresh apply가 이 값들을 바꾸면 data race 또는 서로 다른 snapshot이 생길 수 있다.

Before:

```go
// apps/edge/internal/openai/routes.go:28
var modelIDs []string
if len(s.modelCatalog) > 0 {
	for _, entry := range s.modelCatalog {
```

```go
// apps/edge/internal/openai/chat_handler.go:203
for i := range s.modelCatalog {
	if s.modelCatalog[i].ID == model {
		return &s.modelCatalog[i]
	}
}
```

```go
// apps/edge/internal/service/run_dispatch.go:493
for i := range s.modelCatalog {
	if s.modelCatalog[i].ID == req.ModelGroupKey {
		catalogEntry = &s.modelCatalog[i]
```

### 해결 방법

- `Service`에 `runtimeMu sync.RWMutex`를 추가하고 nodeStore/modelCatalog read/write를 helper로 감싼다.
- `OpenAI Server`에 `catalogMu sync.RWMutex` 또는 atomic snapshot helper를 추가해 `/v1/models`와 `findProviderPoolEntry`가 복사본을 읽게 한다.
- `SetModelCatalog`는 slice copy를 저장하고, read helper는 copy 또는 immutable pointer를 반환한다.
- `modelQueueManager.setStore` 호출은 `Service` commit helper에서 같은 write lock 아래 수행한다.
- `modelQueueManager.getOrCreateGroupLocked`의 기존 group policy retention을 검토하고, provider `max_queue`/`queue_timeout_ms` 변경이 기존 group에도 반영되도록 `updateGroupPolicyLocked` 또는 model group reset policy를 추가한다. In-flight run map은 유지한다.

Before queue behavior:

```go
// apps/edge/internal/service/model_queue.go:140
// getOrCreateGroupLocked returns an existing group or creates one with the
// given policy (applying defaults for zero fields). Policy is only applied
// at creation; existing groups retain their current policy.
func (m *modelQueueManager) getOrCreateGroupLocked(key string, policy groupPolicy) *modelQueueGroup {
```

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

- [ ] `apps/edge/internal/service/service.go`: runtime state lock/helpers 추가.
- [ ] `apps/edge/internal/service/run_dispatch.go`: model catalog/node store read helper 사용.
- [ ] `apps/edge/internal/service/status_provider.go`: node store snapshot read helper 사용.
- [ ] `apps/edge/internal/service/model_queue.go`: store/policy refresh helper 추가.
- [ ] `apps/edge/internal/service/*_test.go`: capacity/queue policy refresh dispatch test 추가.
- [ ] `apps/edge/internal/openai/server.go`: catalog snapshot lock 추가.
- [ ] `apps/edge/internal/openai/routes.go`: `/v1/models`가 refreshed catalog snapshot을 사용.
- [ ] `apps/edge/internal/openai/chat_handler.go`: provider-pool route lookup이 refreshed catalog snapshot을 사용.
- [ ] `apps/edge/internal/openai/server_test.go`: refreshed catalog가 `/v1/models`와 provider-pool route dispatch에 반영되는 테스트 추가.

### 테스트 작성

- 작성: `TestRefreshProviderCapacityAffectsNextDispatch`.
- 작성: `TestRefreshProviderQueuePolicyUpdatesExistingGroup`.
- 작성: `TestOpenAIModelsUsesRefreshedCatalog`.
- 작성: `TestOpenAIProviderPoolDispatchUsesRefreshedCatalog`.

### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai
```

예상: service/openai targeted refresh state tests PASS.

## REFRESH_APPLY-3 - Previous State Regression

### 문제

SDD S10은 invalid provider refresh가 기존 `/v1/models`와 provider dispatch 상태를 깨지 않아야 한다고 요구한다. 현재는 refresh apply 경계가 없어서 이 회귀를 증명하는 테스트도 없다.

### 해결 방법

- 정상 current config로 runtime을 만들고 provider-pool catalog가 `/v1/models`에 보이는 상태를 준비한다.
- 기존 provider dispatch가 성공하는 net.Pipe 기반 service fixture를 구성한다.
- invalid provider config candidate로 apply를 호출한다.
- 결과가 `rejected`이고, 직후 `/v1/models`가 기존 catalog를 반환하며, 같은 model group dispatch가 기존 provider/capacity 기준으로 성공하는지 검증한다.

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

- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: end-to-end rejected refresh preservation test 추가.
- [ ] `apps/edge/internal/service/service_test.go` 또는 새 refresh test: provider dispatch preservation helper 추가.
- [ ] `apps/edge/internal/openai/server_test.go`: `/v1/models` preservation assertion 추가.

### 테스트 작성

- 작성: `TestRefreshRejectedProviderPreservesModelsAndDispatch`.
- 작성: 실패 hook 사용 시 `TestRefreshApplyHookFailurePreservesModelsAndDispatch`.
- invalid candidate는 provider capacity 음수, duplicate provider id, 또는 catalog가 존재하지 않는 provider를 참조하는 fixture 중 하나를 사용한다.

### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service ./apps/edge/internal/openai
```

예상: S10 회귀 테스트 PASS.

## REFRESH_APPLY-4 - Ops Report Surface

### 문제

refresh 결과는 operator가 바뀐 node/provider/model 항목과 최종 status를 확인할 수 있어야 한다. `02_refresh_contract`가 classifier 응답을 제공하더라도 S11 완료를 위해 API/CLI/log/event 중 최소 한 표면에 changed items가 검증되어야 한다.

### 해결 방법

- MVP report surface는 API/CLI JSON response로 고정한다.
- `configrefresh.Result`에 `changed_nodes`, `changed_providers`, `changed_models`, `restart_required_paths`, `rejected_reason`을 추가하거나 기존 `changes[]`에서 stable derived fields를 노출한다.
- CLI는 API response를 그대로 pretty JSON 또는 stable JSON으로 출력한다. 테스트 안정성을 위해 key order가 필요한 assertion은 decode 후 field 비교로 한다.
- Runtime logger에는 `request_id`, `mode`, `status`, `changed_counts`를 남긴다. Log assertion은 필수 완료 근거가 아니며 API/CLI assertion을 S11 근거로 삼는다.

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

- [ ] `apps/edge/internal/configrefresh/*.go`: report fields 추가.
- [ ] `apps/edge/internal/bootstrap/refresh_admin.go`: response에 report fields 포함.
- [ ] `apps/edge/internal/edgecmd/config.go`: CLI 출력에 report fields 유지.
- [ ] `apps/edge/internal/edgecmd/edgecmd_test.go`: changed node/provider/model response assertion 추가.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: API response report assertion 추가.

### 테스트 작성

- 작성: `TestRefreshAPIReportIncludesChangedItems`.
- 작성: `TestConfigRefreshCommandPrintsChangedItems`.

### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/edgecmd ./apps/edge/internal/bootstrap
```

예상: S11 API/CLI report assertions PASS.

## REFRESH_APPLY-5 - Final Evidence

### 문제

`atomic-apply`와 `ops-report`는 코드 결과만으로는 완료 처리되지 않는다. 구현 에이전트가 review stub에 실제 검증 출력과 설계 결정을 남겨야 code-review가 Roadmap Completion 근거를 만들 수 있다.

### 해결 방법

- 최종 검증 명령을 모두 실행한다.
- `CODE_REVIEW-cloud-G08.md`의 구현 항목별 완료 여부, 구현 체크리스트, 계획 대비 변경 사항, 주요 설계 결정, 검증 결과를 실제 내용으로 채운다.
- 02 plan과 API symbol이 달라진 경우 mapping을 기록한다.

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

- [ ] `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/CODE_REVIEW-cloud-G08.md`: 구현 에이전트 소유 섹션 작성.

### 테스트 작성

- 별도 테스트 없음. 이 항목은 evidence 기록 단계다.

### 중간 검증

```bash
go test -count=1 ./apps/edge/... ./packages/go/... ./proto/gen/...
```

예상: Edge, shared config, generated proto package가 모두 PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/bootstrap/runtime.go` | REFRESH_APPLY-1, REFRESH_APPLY-3 |
| `apps/edge/internal/bootstrap/refresh_admin.go` | REFRESH_APPLY-1, REFRESH_APPLY-4 |
| `apps/edge/internal/bootstrap/runtime_test.go` | REFRESH_APPLY-1, REFRESH_APPLY-3, REFRESH_APPLY-4 |
| `apps/edge/internal/configrefresh/*.go` | REFRESH_APPLY-1, REFRESH_APPLY-4 |
| `apps/edge/internal/node/store.go` | REFRESH_APPLY-1 |
| `apps/edge/internal/service/service.go` | REFRESH_APPLY-2 |
| `apps/edge/internal/service/run_dispatch.go` | REFRESH_APPLY-2 |
| `apps/edge/internal/service/status_provider.go` | REFRESH_APPLY-2 |
| `apps/edge/internal/service/model_queue.go` | REFRESH_APPLY-2 |
| `apps/edge/internal/service/*_test.go` | REFRESH_APPLY-2, REFRESH_APPLY-3 |
| `apps/edge/internal/openai/server.go` | REFRESH_APPLY-2 |
| `apps/edge/internal/openai/routes.go` | REFRESH_APPLY-2 |
| `apps/edge/internal/openai/chat_handler.go` | REFRESH_APPLY-2 |
| `apps/edge/internal/openai/server_test.go` | REFRESH_APPLY-2, REFRESH_APPLY-3 |
| `apps/edge/internal/edgecmd/config.go` | REFRESH_APPLY-4 |
| `apps/edge/internal/edgecmd/edgecmd_test.go` | REFRESH_APPLY-4 |
| `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/CODE_REVIEW-cloud-G08.md` | REFRESH_APPLY-5 |

## 최종 검증

```bash
find agent-task -path '*/m-runtime-reconnect-config-refresh/02_refresh_contract/complete.log' -print -quit
```

예상: active 또는 archived 02 predecessor complete.log 경로가 한 줄 출력된다.

```bash
go test -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/edgecmd
```

예상: atomic apply, OpenAI model surface, provider dispatch, CLI/API report targeted tests PASS.

```bash
go test -count=1 ./apps/edge/... ./packages/go/... ./proto/gen/...
```

예상: 모든 Edge/config/proto generated package 테스트 PASS.

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