diff --git a/IOP_ROADMAP_STATUS.md b/IOP_ROADMAP_STATUS.md
index f06f303..83958ea 100644
--- a/IOP_ROADMAP_STATUS.md
+++ b/IOP_ROADMAP_STATUS.md
@@ -1,6 +1,6 @@
# IOP 로드맵 현황 및 진행 계획
-작성 기준: 2026-06-18
+작성 기준: 2026-06-20
# 1. 월별 로드맵
@@ -21,11 +21,11 @@
| 구분 | 내용 |
| --- | --- |
-| 목표 | 같은 Edge 안에서 동일 `model` 요청을 model group queue로 묶고, 여러 Node 후보에 순차 dispatch하는 구조를 정리합니다. Lemonade 이후 vLLM과 SGLang provider 검증을 이어가며 1차 KPI 범위의 속도/디바이스 효율 축을 닫습니다. |
+| 목표 | 같은 Edge 안에서 동일 `model` 요청을 model group queue로 묶고, 여러 Node 후보에 순차 dispatch하는 구조를 정리합니다. Lemonade 이후 vLLM과 SGLang provider 검증 결과를 반영해 1차 KPI 범위의 속도/디바이스 효율 축을 닫습니다. |
| 예상 투입인원 | 1명 |
-| 현재 구현 상태 | Node-local admission/capacity 기반 완료
Edge model group queue scheduling과 같은 Edge 내부의 다중 Node 후보 순차 dispatch 기반 완료
OpenAI-compatible `model` route queue 정책 정합화 완료
vLLM provider serving validation 완료: provider boundary, DGX Spark vLLM container, config contract, 모델 조회/chat, streaming, split-host field smoke 검증 PASS 근거와 코드 레벨 완료 리뷰까지 정리하고 archive함
SGLang provider serving validation을 다음 활성 마일스톤으로 지정함. 현재 계획/잠금 상태이며 SDD 승인과 provider 경계·실테스트 endpoint/model/auth/streaming 결정이 필요함 |
-| 주요 작업 | 완료: OpenAI-compatible `model` 값을 queue group key로 사용하는 Edge-owned FIFO 정리
완료: Node 후보별 capacity, in-flight, queued snapshot을 기준으로 dispatch 흐름 정리
완료: Model route queue 정책 원천 정합화
완료: vLLM OpenAI-compatible provider의 config contract, 모델 조회, chat, streaming, split-host field smoke 검증
다음: SGLang provider SDD gate 해소와 최소 serving 경로 검증
계획: Provider별 adapter/config/target/model 매핑 기준을 vLLM/SGLang 검증 결과로 보강 |
-| 완성 예정 기능 | vLLM provider 최소 serving 검증
SGLang provider 최소 serving 검증
Provider별 adapter/config/target/model 매핑 기준 보강 |
+| 현재 구현 상태 | Node-local admission/capacity 기반 완료
Edge model group queue scheduling과 같은 Edge 내부의 다중 Node 후보 순차 dispatch 기반 완료
OpenAI-compatible `model` route queue 정책 정합화 완료
vLLM provider serving validation 완료: provider boundary, DGX Spark vLLM container, config contract, 모델 조회/chat, streaming, split-host field smoke 검증 PASS 근거와 코드 레벨 완료 리뷰까지 정리하고 archive함
SGLang provider serving validation은 GX10 + Qwen3.6 35B NVFP4 field validation 결과 NVIDIA checkpoint 직접 로딩 실패와 vLLM 대비 낮은 throughput/KV 여유가 확인되어 현 조건에서 provider 채택을 폐기함
다음 활성 작업은 운영 관측 Phase의 `Model Alias Provider Pool과 Provider Catalog` 계획 마일스톤으로 전환됨 |
+| 주요 작업 | 완료: OpenAI-compatible `model` 값을 queue group key로 사용하는 Edge-owned FIFO 정리
완료: Node 후보별 capacity, in-flight, queued snapshot을 기준으로 dispatch 흐름 정리
완료: Model route Queue 정책 원천 정합화
완료: vLLM OpenAI-compatible provider의 config contract, 모델 조회, chat, streaming, split-host field smoke 검증
정리: SGLang provider는 현 field 조건에서 채택하지 않고 Provider Catalog/Qualification 후속 판단 후보로 분리
다음: `models[]` 기반 provider pool과 read-only Provider Catalog 계약 구현 계획 |
+| 완성 예정 기능 | vLLM provider 최소 serving 검증 완료
SGLang provider field 판단 결과 정리
Provider별 adapter/config/target/model 매핑 기준을 Provider Catalog 계약으로 승계 |
---
@@ -35,21 +35,21 @@
| --- | --- |
| 목표 | IOP를 여러 Edge, Node, CLI Agent, local inference provider와 함께 운영하기 위해 필요한 사용자/토큰/사용량/로그/provider 상태 관찰 기준을 1차 feature MVP 단위로 정리합니다. |
| 예상 투입인원 | 1명 |
-| 현재 구현 상태 | Edge, Node, Control Plane, Client의 기본 상태 관찰 경로 구축
Provider availability와 capacity snapshot 기반 완료
사용자/토큰/사용량/로그 추적과 Provider Catalog는 스케치 상태이며, 사용자 검토 후 계획으로 승격 |
-| 주요 작업 | 사용자 단위 API/CLI/local inference 사용량 집계 범위 정리
Token, request, execution, provider usage 로그의 최소 수집 항목 정의
Control Plane/Client 또는 CLI에서 확인할 운영 상태 항목 정리
API, CLI, local inference provider를 구분하는 Provider Catalog 기준 정리
vLLM, vLLM-MLX, Lemonade, SGLang 등 로컬 디바이스 provider 상태 표현 정리 |
-| 완성 예정 기능 | 사용량, 토큰, 로그 운영 추적 MVP
Provider Catalog와 로컬 디바이스 상태 관리 MVP
운영자가 확인할 수 있는 provider/status/usage 최소 화면 또는 명령 표면 |
+| 현재 구현 상태 | Edge, Node, Control Plane, Client의 기본 상태 관찰 경로 구축
Provider availability와 capacity snapshot 기반 완료
`Model Alias Provider Pool과 Provider Catalog`가 현재 활성 계획 마일스톤이며 SDD는 승인됨/잠금 해제 상태
사용자/토큰/사용량/로그 추적과 Provider-Device-Model Qualification 리포트는 후속 스케치 상태 |
+| 주요 작업 | `models[]`를 OpenAI-compatible model key이자 provider pool canonical routing key로 정리
`nodes[].providers[]` 기반 provider 정의와 provider별 served model list 계약 정리
Edge-owned target rewrite와 `in_flight / capacity` load-ratio provider 선택 정책 구현 계획
API, CLI, local inference provider를 구분하는 read-only Provider Catalog 기준 정리
사용자 단위 API/CLI/local inference 사용량, token, request, execution 로그의 최소 수집 항목은 후속 MVP로 구체화 |
+| 완성 예정 기능 | Model Alias Provider Pool과 Provider Catalog MVP
Provider Catalog와 로컬 디바이스 상태 관리 MVP
사용량, 토큰, 로그 운영 추적 MVP
운영자가 확인할 수 있는 provider/status/usage 최소 화면 또는 명령 표면 |
---
-## 8월 ~ 9월: CLI Agent 알림, 원격 작업 환경, 단계 호출 검증 MVP
+## 8월 ~ 9월: CLI Agent 알림, 원격 작업 환경, Update Plane, 단계 호출 검증 MVP
| 구분 | 내용 |
| --- | --- |
-| 목표 | CLI Agent 사용량 limit 감지와 알림, 작업 자동 이어받기, workspace-bound execution 기반 원격 작업 환경을 feature MVP로 정리합니다. 동시에 planner/generator/verifier 형태의 단계 호출과 runtime schema 검증은 최소 실행 모드로 스케치합니다. |
+| 목표 | CLI Agent 사용량 limit 감지와 알림, 작업 자동 이어받기, workspace-bound execution 기반 원격 작업 환경을 feature MVP로 정리합니다. 동시에 Edge/Node 자체 업데이트 기반과 planner/generator/verifier 형태의 단계 호출/runtime schema 검증은 최소 실행 모드로 스케치합니다. |
| 예상 투입인원 | 1명 |
-| 현재 구현 상태 | CLI runtime과 usage checker 계열 기반 마련
알림 자체는 `nexo`의 messaging/notification 기반을 활용할 수 있는 상태
socket/protocol 기반은 `proto-socket`의 안정화 결과를 활용할 수 있는 상태
CLI Agent 알림/자동 이어받기, 원격 작업 환경, 단계 호출 검증 MVP는 스케치 상태이며 사용자 검토 후 계획으로 승격 |
-| 주요 작업 | CLI Agent limit 도달 이벤트와 사용자 알림 trigger 기준 정리
자동 이어받기 허용 조건, 중단 조건, 사용자 승인 경계 정리
workspace-bound execution을 이용한 원격 코딩/유지보수 작업 환경의 최소 운영 흐름 정리
요청 의도 분석, 실제 작업, 검증/schema 강제, 오류 시 retry/fallback의 최소 단계 실행 흐름 정리
Nexo 알림 기반과 IOP 실행 이벤트의 연동 경계 정리 |
-| 완성 예정 기능 | CLI Agent 사용량 알림 MVP
CLI Agent 자동 이어받기 MVP
원격 코딩/유지보수 작업 환경 MVP
단계 호출과 runtime schema 검증 MVP |
+| 현재 구현 상태 | CLI runtime과 usage checker 계열 기반 마련
OpenAI Workspace Agent Execution Contract 완료
알림 자체는 `nexo`의 messaging/notification 기반을 활용할 수 있는 상태
socket/protocol 기반은 `proto-socket`의 안정화 결과를 활용할 수 있는 상태
CLI Agent 알림/자동 이어받기, 원격 작업 환경, Update Plane, 단계 호출 검증 MVP는 스케치/계획 상태이며 사용자 검토 후 구체화 |
+| 주요 작업 | CLI Agent limit 도달 이벤트와 사용자 알림 trigger 기준 정리
자동 이어받기 허용 조건, 중단 조건, 사용자 승인 경계 정리
workspace-bound execution을 이용한 원격 코딩/유지보수 작업 환경의 최소 운영 흐름 정리
Control Plane release view, Edge/Node update protocol, host-local manager, rollout/recovery 정책 스케치
요청 의도 분석, 실제 작업, 검증/schema 강제, 오류 시 retry/fallback의 최소 단계 실행 흐름 정리
Nexo 알림 기반과 IOP 실행 이벤트의 연동 경계 정리 |
+| 완성 예정 기능 | CLI Agent 사용량 알림 MVP
CLI Agent 자동 이어받기 MVP
원격 코딩/유지보수 작업 환경 MVP
Update Plane 안정 프로토콜과 자체 업데이트 기반 스케치
단계 호출과 runtime schema 검증 MVP |
---
@@ -80,9 +80,9 @@
| 구분 | 주요 산출물 |
| ----------------| -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 5월 ~ 6월 중순 | Edge-Node 실행 기반, Ollama serving 안정화, Control Plane/Client 운영 기반, CLI Automation Runtime, Domain Agent registry/bootstrap, OpenAI Responses input surface, Workspace Agent Execution Contract, Workspace port/env 표준화, Lemonade provider 검증 결과 |
-| 6월 중순 ~ 7월 | Edge model group queue scheduling과 model route queue 정책 정합화 결과, vLLM/SGLang provider 검증 결과, provider별 adapter/config/target/model 매핑 기준 |
-| 7월 ~ 8월 | 사용량/토큰/로그 추적 MVP 정의, Provider Catalog 초안, 로컬 디바이스 provider 상태 관리 기준, 운영 확인 표면 초안 |
-| 8월 ~ 9월 | CLI Agent 사용량 알림/자동 이어받기 MVP, 원격 작업 환경 MVP, 단계 호출과 runtime schema 검증 MVP, Nexo 알림 연동 경계 |
+| 6월 중순 ~ 7월 | Edge model group queue scheduling과 model route queue 정책 정합화 결과, vLLM provider 검증 결과, SGLang provider field 판단 결과, provider별 adapter/config/target/model 매핑 기준 |
+| 7월 ~ 8월 | Model Alias Provider Pool과 Provider Catalog MVP, 사용량/토큰/로그 추적 MVP 정의, 로컬 디바이스 provider 상태 관리 기준, 운영 확인 표면 초안 |
+| 8월 ~ 9월 | CLI Agent 사용량 알림/자동 이어받기 MVP, 원격 작업 환경 MVP, Update Plane 안정 프로토콜/자체 업데이트 기반, 단계 호출과 runtime schema 검증 MVP, Nexo 알림 연동 경계 |
| 9월 ~ 10월 | 1차 KPI 범위 feature MVP 통합 검증 결과, E2E smoke 기준, 운영 전환 체크리스트, known limitation, 2차 범위 분리 결과 |
| 10월 이후 | 장기 기억/RAG, advisor/context hook, 원격 터널링, oto scheduler/CI-CD, cloud fallback/품질 평가 고도화 검토 결과 |
@@ -92,7 +92,7 @@
| 리스크 및 고려사항 | 내용 |
| --- | --- |
-| Provider 실장비 검증 | vLLM, SGLang, vLLM-MLX 등 local inference provider는 adapter 구현보다 실제 장비와 런타임별 동작 검증이 일정 변수입니다. 1차 범위에서는 provider별 전체 최적화보다 최소 serving 경로와 상태 표현을 우선합니다. |
+| Provider 실장비 검증 | vLLM, SGLang, vLLM-MLX 등 local inference provider는 adapter 구현보다 실제 장비와 런타임별 동작 검증이 일정 변수입니다. SGLang은 현 field 조건에서 채택하지 않기로 정리했으며, 1차 범위에서는 provider별 전체 최적화보다 최소 serving 경로와 상태 표현을 우선합니다. |
| 사용량/토큰/로그 정책 결정 | 사용자 단위 usage, token, log 추적은 구현보다 보존 범위, 노출 범위, 제한 정책 결정이 중요합니다. Billing, chargeback, 조직 IAM, 장기 retention은 1차 범위에서 완성하지 않고 후속 구체화로 둡니다. |
| CLI Agent 자동 이어받기 안전 경계 | 자동 이어받기는 편의성이 크지만, 무제한 재시도나 의도하지 않은 실행 지속으로 이어지면 위험합니다. 1차에서는 limit 감지, 알림, 명시된 조건에서의 이어받기만 MVP로 다룹니다. |
| Nexo와 Proto Socket 기반 활용 | 알림 기반은 `nexo`, socket/protocol 기반은 `proto-socket`의 거의 완료된 기반을 활용하는 전제로 봅니다. IOP에서는 새 인프라를 다시 만들기보다 실행 이벤트, 알림 trigger, protocol compatibility, 운영 표면 고정에 집중합니다. |
@@ -104,10 +104,10 @@
IOP는 현재 Control Plane - Edge - Node 기반의 실행 오케스트레이션 구조와 OpenAI-compatible 실행 표면을 상당 부분 구축한 상태입니다. Edge-Node 실행 경로, Ollama serving, Control Plane/Client 운영 기반, CLI Automation Runtime, Domain Agent registry/bootstrap, OpenAI Responses input surface, workspace-bound agent execution contract, Workspace port/env 표준화, Node multi-target serving, provider availability/capacity queue, Lemonade provider 검증, Edge model group queue scheduling, model route queue 정책 정합화까지 완료되었습니다.
-현재 vLLM provider serving validation은 완료 및 archive 처리되었습니다. Edge model group queue scheduling과 model route queue 정책 정합화는 완료된 기반으로 보고, 다음 활성 마일스톤은 SGLang provider serving validation입니다. SGLang은 현재 계획/잠금 상태이며, SDD 승인과 provider 경계·실테스트 endpoint/model/auth/streaming 결정이 다음 진입 조건입니다.
+현재 vLLM provider serving validation은 완료 및 archive 처리되었습니다. SGLang provider serving validation은 field validation 결과를 반영해 현 조건에서는 provider 채택을 폐기했습니다. Edge model group queue scheduling과 model route queue 정책 정합화는 완료된 기반으로 보고, 다음 활성 마일스톤은 운영 관측 Phase의 `Model Alias Provider Pool과 Provider Catalog`입니다.
-이후 7월부터 8월까지는 사용자/토큰/사용량/로그 추적과 Provider Catalog, 로컬 디바이스 provider 상태 관리를 정리합니다. 이 단계는 완성된 billing이나 enterprise IAM이 아니라, 운영자가 IOP의 사용량과 provider 상태를 확인할 수 있는 feature MVP를 목표로 합니다.
+이후 7월부터 8월까지는 `models[]` 기반 provider pool, Edge-owned target rewrite, load-ratio provider 선택, read-only Provider Catalog, 로컬 디바이스 provider 상태 관리를 정리합니다. 이 단계는 완성된 billing이나 enterprise IAM이 아니라, 운영자가 IOP의 사용량과 provider 상태를 확인할 수 있는 feature MVP를 목표로 합니다. 사용자/토큰/사용량/로그 추적과 Provider-Device-Model Qualification 리포트는 후속 마일스톤으로 이어집니다.
-8월부터 9월까지는 CLI Agent 사용량 limit 알림, 자동 이어받기, 원격 코딩/유지보수 작업 환경, 단계 호출과 runtime schema 검증 MVP를 구체화합니다. 알림 기반은 `nexo`, socket/protocol 기반은 `proto-socket`의 거의 완료된 기반을 활용하고, IOP는 실행 이벤트와 운영 정책, E2E 흐름 고정에 집중합니다.
+8월부터 9월까지는 CLI Agent 사용량 limit 알림, 자동 이어받기, 원격 코딩/유지보수 작업 환경, Update Plane 자체 업데이트 기반, 단계 호출과 runtime schema 검증 MVP를 구체화합니다. 알림 기반은 `nexo`, socket/protocol 기반은 `proto-socket`의 거의 완료된 기반을 활용하고, IOP는 실행 이벤트와 운영 정책, 업데이트 수렴, E2E 흐름 고정에 집중합니다.
9월부터 10월까지는 1차 KPI 범위에 포함된 feature MVP를 통합 검증하고 안정화합니다. 10월 이후에는 장기 기억/RAG, advisor, context hook, 원격 터널링, oto scheduler/CI-CD, cross-Edge/cloud fallback 같은 2차 고도화 후보를 별도로 검토합니다.
diff --git a/agent-roadmap/ROADMAP.md b/agent-roadmap/ROADMAP.md
index a154dfd..d6c07c5 100644
--- a/agent-roadmap/ROADMAP.md
+++ b/agent-roadmap/ROADMAP.md
@@ -47,7 +47,7 @@ provider/device/model별 qualification report와 모델 lifecycle 관리는 prov
- 경로: `agent-roadmap/phase/inference-provider-extension/PHASE.md`
- 요약: 1차 MVP의 속도 향상 축으로, 같은 Edge 안의 model group queue와 여러 Node 후보 순차 dispatch를 기준으로 다중 디바이스 효율을 높이고 Lemonade/vLLM/SGLang 같은 provider 검증을 이어가는 단계다.
-- [계획] 운영 관측과 Provider 관리
+- [진행중] 운영 관측과 Provider 관리
- 경로: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- 요약: 사용자/토큰/사용량/로그 추적과 API/CLI/local inference provider catalog, 로컬 디바이스 provider 상태 관리, provider/device/model qualification report와 모델 lifecycle 관리 방향을 MVP 운영 축과 후속 심화 축으로 스케치한다.
diff --git a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md
index 1e8c1e7..01483ee 100644
--- a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md
+++ b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md
@@ -2,7 +2,7 @@
## 상태
-[계획]
+[진행중]
## 목표
@@ -16,7 +16,7 @@ provider 확장 Phase에서 검증한 Ollama, vLLM, SGLang, Lemonade 같은 추
완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
스케치 Milestone은 아직 구현 가능한 계획이 아니므로 사용자 검토와 구체화 후 `[계획]`으로 승격한다.
-- [계획] Model Alias Provider Pool과 Provider Catalog
+- [진행중] Model Alias Provider Pool과 Provider Catalog
- 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- 요약: `models[]`를 외부 model key이자 운영 catalog model로 두고, Node 하위 provider와 served model list를 기준으로 Edge가 provider 선택, load-ratio routing, target rewrite, read-only catalog 상태를 소유하는 MVP 계약을 구현 계획 대상으로 정리한다.
diff --git a/agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md b/agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md
index 9490ddb..5c1e210 100644
--- a/agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md
+++ b/agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md
@@ -14,7 +14,7 @@ provider/device/model별 qualification report와 benchmark/품질 비교는 이
## 상태
-[계획]
+[진행중]
## 승격 조건
@@ -52,27 +52,27 @@ provider/device/model별 qualification report와 benchmark/품질 비교는 이
`models[]` catalog model을 중심으로 provider 후보를 묶고 Edge가 실행 후보를 고르는 최소 산출물을 구현한다.
-- [ ] [alias-contract] `models[].id`가 OpenAI-compatible `model` key이자 provider pool의 canonical routing key가 되며, `models[].providers`가 provider id -> served model name map으로 검증된다.
-- [ ] [candidate-schema] `nodes[].providers[]`가 provider id, type, category, served `models[]`, health, capacity, in-flight, queued, coarse lifecycle capability를 표현한다.
-- [ ] [target-rewrite] Edge가 선택한 provider id의 `models[].providers[provider_id]` 값을 concrete target으로 rewrite해 Node에 전달하고, Node/provider adapter는 rewrite하지 않는다.
-- [ ] [selection-policy] Edge가 available provider 중 `in_flight / capacity` load ratio가 가장 낮은 후보를 우선 선택하고, 동률은 deterministic tie-break로 처리하며, queue timeout을 적용한다.
-- [ ] [pool-compat] 기존 `openai.model_routes`, `adapter + target`, Node provider instance 설정과 새 `models[]`/provider catalog 계약의 호환/마이그레이션 방향이 정리되어 있다.
+- [x] [alias-contract] `models[].id`가 OpenAI-compatible `model` key이자 provider pool의 canonical routing key가 되며, `models[].providers`가 provider id -> served model name map으로 검증된다.
+- [x] [candidate-schema] `nodes[].providers[]`가 provider id, type, category, served `models[]`, health, capacity, in-flight, queued, coarse lifecycle capability를 표현한다.
+- [x] [target-rewrite] Edge가 선택한 provider id의 `models[].providers[provider_id]` 값을 concrete target으로 rewrite해 Node에 전달하고, Node/provider adapter는 rewrite하지 않는다.
+- [x] [selection-policy] Edge가 available provider 중 `in_flight / capacity` load ratio가 가장 낮은 후보를 우선 선택하고, 동률은 deterministic tie-break로 처리하며, queue timeout을 적용한다.
+- [x] [pool-compat] 기존 `openai.model_routes`, `adapter + target`, Node provider instance 설정과 새 `models[]`/provider catalog 계약의 호환/마이그레이션 방향이 정리되어 있다.
### Epic: [provider-catalog] Provider Operations Catalog
운영자가 model provider pool과 provider 상태를 같은 기준으로 비교하기 위한 read-only catalog 산출물을 묶는다.
-- [ ] [category-map] MVP provider category가 `api`, `cli`, `local_inference`로 정리되고 read-only catalog 표시 필드에 반영된다.
+- [x] [category-map] MVP provider category가 `api`, `cli`, `local_inference`로 정리되고 read-only catalog 표시 필드에 반영된다.
- [ ] [device-status] provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
-- [ ] [lifecycle-cap] Ollama, Lemonade, vLLM, SGLang 등의 lifecycle 차이가 coarse capability flags로 표시되고 상세 qualification report는 후속 범위로 분리된다.
-- [ ] [boundary] provider adapter 구현, Edge-owned provider pool routing, read-only operations catalog, 후속 runtime control의 책임 경계가 정리되어 있다.
+- [x] [lifecycle-cap] Ollama, Lemonade, vLLM, SGLang 등의 lifecycle 차이가 coarse capability flags로 표시되고 상세 qualification report는 후속 범위로 분리된다.
+- [x] [boundary] provider adapter 구현, Edge-owned provider pool routing, read-only operations catalog, 후속 runtime control의 책임 경계가 정리되어 있다.
- [ ] [ops-review] Edge config와 Edge-owned read-only catalog/status를 기준으로 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
-- 완료 근거: 계획 Milestone이며 model provider pool과 catalog 기능 Task가 아직 충족되지 않았다.
+- 완료 근거: 진행중 Milestone이며 `agent-task/archive/2026/06/m-provider-catalog-device-status/01_pool_schema/complete.log`와 `agent-task/archive/2026/06/m-provider-catalog-device-status/02+01_edge_dispatch/complete.log`의 Roadmap/Spec Completion 근거로 `alias-contract`, `candidate-schema`, `target-rewrite`, `selection-policy`를 완료 반영했다. `pool-compat`는 `01_pool_schema` 계획의 API-3 compatibility 산출물, `apps/edge/README.md`와 `configs/edge.yaml`의 migration/compat note, `TestLoadEdge_ProviderPoolLegacyCompatibility` 및 OpenAI provider-pool legacy fallback tests를 S05 보조 근거로 완료 반영했다. Provider Operations Catalog는 `packages/go/config`, `proto/iop/runtime.proto`, `apps/edge/internal/service/model_queue.go`, `apps/edge/README.md`, `configs/edge.yaml`, `apps/edge/internal/edgecmd/nodes.go` 근거로 `category-map`, `lifecycle-cap`, `boundary`를 완료 반영했으며, `device-status`와 `ops-review`는 Control Plane/Client 표면 계획이 남아 있다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
@@ -97,4 +97,7 @@ provider/device/model별 qualification report와 benchmark/품질 비교는 이
- 선행 작업: Node provider 상태와 Capacity Queue 기반, Edge 모델 그룹 Queue 스케줄링 전환
- 후속 작업: provider pool scheduler 구현, Provider-Device-Model Qualification 리포트와 Lifecycle 관리, provider enable/disable, benchmark/품질 평가
- 작업 메모: [pool-compat] `apps/edge/README.md`와 `configs/edge.yaml`에 기존 `openai.model_routes`/`adapter + target`/Node provider instance 설정을 새 `models[]` provider pool의 1:1 migration 입력 및 fallback/compat 경로로 유지하는 방향을 정리했다. SDD 대상 Milestone의 공식 Task 체크는 `complete.log`의 Roadmap/Spec Completion 근거로 처리한다.
+- 작업 메모: [sync] `01_pool_schema` 완료 로그로 `alias-contract`, `candidate-schema`를, `02+01_edge_dispatch` 완료 로그로 `target-rewrite`, `selection-policy`를 Roadmap/Spec Completion 기준 완료 처리했다.
+- 작업 메모: [sync-correction] `pool-compat`는 `01_pool_schema`의 공식 Roadmap/Spec Targets에는 빠졌지만, SDD S05 Evidence Map이 요구한 migration/compat note와 기존 `model_routes` regression evidence가 `apps/edge/README.md`, `configs/edge.yaml`, `packages/go/config/config_test.go`, `apps/edge/internal/openai/server_test.go`에 남아 있어 완료로 보정했다.
+- 작업 메모: [provider-catalog-small] `api`, `cli`, `local_inference` category는 `config.Category*`와 `ProviderSnapshot.category`에 반영되어 있고, coarse lifecycle capability는 `nodes[].providers[].lifecycle_capabilities`와 `ProviderSnapshot.lifecycle_capabilities` 및 `iop-edge nodes list` configured provider 요약으로 확인 가능하다. 상세 lifecycle/qualification과 runtime control은 SDD/범위 제외 기준에 따라 후속 Milestone으로 분리한다.
- 확인 필요: 없음
diff --git a/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G03_1.log b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G03_1.log
new file mode 100644
index 0000000..7eaeb2d
--- /dev/null
+++ b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G03_1.log
@@ -0,0 +1,198 @@
+
+
+# Code Review Reference - REVIEW_API
+
+> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
+> The task is NOT complete until every implementation-owned section below is filled in.
+> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
+> Fill implementation-owned sections, then stop with active files in place and report ready for review.
+> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
+> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review.
+> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
+> Follow the ownership table at the bottom of this file for which sections you own.
+
+## 개요
+
+date=2026-06-20
+task=m-provider-catalog-device-status/03_catalog_status_http, plan=1, tag=REVIEW_API
+
+## Roadmap Targets
+
+- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- Task ids:
+ - `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
+- Completion mode: check-on-pass
+
+## Spec Targets
+
+- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- Acceptance scenarios:
+ - `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
+- Completion mode: spec-check-on-pass
+
+## Archive Evidence Snapshot
+
+- Archived plan: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
+- Archived review: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
+- Verdict: FAIL
+- Required summary:
+ - `apps/control-plane/cmd/control-plane/http_views.go:158` uses `json:"load_ratio,omitempty"`, so a valid idle provider with `load_ratio=0` omits the required status field from `/edges/{id}/status` JSON.
+ - `apps/control-plane/cmd/control-plane/main_test.go:638` already has a second provider fixture with implicit zero load ratio, but raw JSON assertions do not verify that `load_ratio` is present and equals `0`.
+- Affected files:
+ - `apps/control-plane/cmd/control-plane/http_views.go`
+ - `apps/control-plane/cmd/control-plane/main_test.go`
+- Prior verification evidence:
+ - Implementer recorded `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` as passing.
+ - Review rerun confirmed `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` pass before this follow-up.
+- Carryover:
+ - Roadmap Targets and Spec Targets remain unchanged.
+ - This follow-up still works toward SDD `S07` and Milestone task `device-status`.
+- Narrow archive reread allowed if needed:
+ - `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
+ - `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
+
+## 이 파일을 읽는 리뷰 에이전트에게
+
+> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
+
+각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
+리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
+
+1. 판정을 append한다.
+2. `CODE_REVIEW-local-G03.md` -> `code_review_local_G03_1.log`, `PLAN-local-G03.md` -> `plan_local_G03_1.log`로 아카이브한다.
+3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
+4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
+5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
+
+---
+
+## 구현 항목별 완료 여부
+
+| 항목 | 완료 여부 |
+|------|---------|
+| [REVIEW_API-1] Zero Load Ratio JSON 보존 | [x] |
+
+## 구현 체크리스트
+
+- [x] `apps/control-plane/cmd/control-plane/http_views.go`에서 `providerSnapshotView.LoadRatio` JSON tag를 `json:"load_ratio"`로 바꿔 zero value도 직렬화한다.
+- [x] `apps/control-plane/cmd/control-plane/main_test.go`에서 second provider raw JSON assertion에 `load_ratio` key 존재 여부와 `float64(0)` 값을 추가한다.
+- [x] `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`를 실행한다.
+- [x] `go test ./apps/control-plane/... -count=1`와 `git diff --check`를 실행한다.
+- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
+
+## 코드리뷰 전용 체크리스트
+
+> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
+> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
+
+- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
+- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
+- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G03_1.log`로 아카이브한다.
+- [x] active `PLAN-*-G??.md`를 `plan_local_G03_1.log`로 아카이브한다.
+- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
+- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
+- [x] PASS이면 active task 디렉터리 `agent-task/m-provider-catalog-device-status/03_catalog_status_http/`를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
+- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
+- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-provider-catalog-device-status/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
+- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-*-G??.md`와 `CODE_REVIEW-*-G??.md`를 작성하고 `complete.log`를 작성하지 않는다.
+- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
+- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
+
+## 계획 대비 변경 사항
+
+계획에 따라 정확한 2개 파일만 수정했다. 추가 변경 사항 없음.
+
+1. `apps/control-plane/cmd/control-plane/http_views.go`: `LoadRatio` JSON tag에서 `omitempty` 제거
+2. `apps/control-plane/cmd/control-plane/main_test.go`: second provider raw JSON에 `load_ratio: 0` assertion 추가
+
+## 주요 설계 결정
+
+없음. 단일 JSON tag 조정만 수행.
+
+## 사용자 리뷰 요청
+
+_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
+
+- 상태: 없음
+- 사유 유형: 없음
+- 연결 대상: 없음
+- 결정 필요: 없음
+- 차단 근거: 없음
+- 실행한 검증/명령: 없음
+- 자동 후속 불가 이유: 없음
+- 재개 조건: 없음
+
+## 리뷰어를 위한 체크포인트
+
+- `LoadRatio` JSON tag가 `omitempty` 없이 zero value를 직렬화하는지 확인한다.
+- raw JSON assertion이 second provider의 `load_ratio: 0` 존재 여부를 검증하는지 확인한다.
+- SDD `S07`의 status JSON evidence가 계속 충족되는지 확인한다.
+
+## 검증 결과
+
+_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
+
+필수 규칙:
+- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
+- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
+- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
+- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
+- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
+
+### REVIEW_API-1 중간 검증
+
+```text
+$ go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
+ok iop/apps/control-plane/cmd/control-plane 0.008s
+```
+
+### 최종 검증
+
+```text
+$ go test ./apps/control-plane/... -count=1
+ok iop/apps/control-plane/cmd/control-plane 0.148s
+
+ok iop/apps/control-plane/internal/wire 1.487s
+
+$ git diff --check
+(no output - no whitespace errors)
+```
+
+---
+
+> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
+> If anything is blank, go back and fill it in before saving this file.
+> Leave review-agent-only sections unchanged.
+
+## 섹션 소유권
+
+| Section | Owner | Note |
+|---------|-------|------|
+| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
+| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
+| Spec Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
+| Archive Evidence Snapshot | Fixed at stub creation from follow-up plan | Implementing agent may narrowly reread only the listed archive paths if needed |
+| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
+| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
+| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
+| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
+| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected SDD/Milestone lock decision blocks implementation |
+| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
+| 검증 결과 | Implementing agent | Paste actual command output |
+
+## 코드리뷰 결과
+
+- 종합 판정: PASS
+- 차원별 평가:
+ - Correctness: Pass
+ - Completeness: Pass
+ - Test coverage: Pass
+ - API contract: Pass
+ - Code quality: Pass
+ - Plan deviation: Pass
+ - Verification trust: Pass
+ - Spec conformance: Pass
+- 발견된 문제:
+ - 없음
+- 다음 단계:
+ - PASS complete.log 작성 후 task directory archive 이동.
diff --git a/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log
new file mode 100644
index 0000000..eb18519
--- /dev/null
+++ b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log
@@ -0,0 +1,206 @@
+
+
+# Code Review Reference - API
+
+> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
+> The task is NOT complete until every implementation-owned section below is filled in.
+> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
+> Fill implementation-owned sections, then stop with active files in place and report ready for review.
+> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
+> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review.
+> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
+> Follow the ownership table at the bottom of this file for which sections you own.
+
+## 개요
+
+date=2026-06-20
+task=m-provider-catalog-device-status/03_catalog_status_http, plan=0, tag=API
+
+## Roadmap Targets
+
+- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- Task ids:
+ - `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
+- Completion mode: check-on-pass
+
+## Spec Targets
+
+- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- Acceptance scenarios:
+ - `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
+- Completion mode: spec-check-on-pass
+
+## 이 파일을 읽는 리뷰 에이전트에게
+
+> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
+
+각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
+리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
+
+1. 판정을 append한다.
+2. `CODE_REVIEW-local-G04.md` -> `code_review_local_G04_0.log`, `PLAN-local-G04.md` -> `plan_local_G04_0.log`로 아카이브한다.
+3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
+4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
+5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
+
+---
+
+## 구현 항목별 완료 여부
+
+| 항목 | 완료 여부 |
+|------|---------|
+| [API-1] Provider Snapshot JSON DTO 추가 | [x] |
+| [API-2] Status JSON 회귀 테스트 보강 | [x] |
+
+## 구현 체크리스트
+
+- [x] Control Plane HTTP status DTO에 provider snapshot read-only fields를 추가하고 `EdgeNodeSnapshot.ProviderSnapshots`를 JSON으로 전달한다.
+- [x] `/edges/{id}/status` 테스트에 provider snapshot fixture와 JSON assertion을 추가해 `health`, `capacity`, `in_flight`, `load_ratio`, `served_models`, `queued`, `category`, `lifecycle_capabilities`가 보존되는지 확인한다.
+- [x] raw config secret non-leak 검증이 provider snapshot 추가 후에도 유지되는지 확인한다.
+- [x] `go test ./apps/control-plane/...`를 실행한다.
+- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
+
+## 코드리뷰 전용 체크리스트
+
+> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
+> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
+
+- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
+- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
+- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G04_0.log`로 아카이브한다.
+- [x] active `PLAN-*-G??.md`를 `plan_local_G04_0.log`로 아카이브한다.
+- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
+- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
+- [ ] PASS이면 active task 디렉터리 `agent-task/m-provider-catalog-device-status/03_catalog_status_http/`를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
+- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
+- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-provider-catalog-device-status/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
+- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G03.md`와 `CODE_REVIEW-local-G03.md`를 작성하고 `complete.log`를 작성하지 않는다.
+- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
+- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
+
+## 계획 대비 변경 사항
+
+계획에 명시된 구현을 그대로 따랐습니다. 추가 변경 사항은 없습니다.
+
+- `providerSnapshotView` struct는 plan의 목표 코드와 동일하게 12개 필드로 추가
+- `providerSnapshotViews` helper는 proto getter만 사용하며 nil/empty slice는 nil 반환
+- `edgeStatusResponseViewFromProto`는 `n.GetProviderSnapshots()`를 `providerSnapshotViews()`로 변환하여 매핑
+- 테스트 fixture는 plan에 명시된 2개 provider snapshot (vllm-gpu, ollama-cpu) 추가
+- typed DTO assertion과 raw JSON assertion 모두 plan에 명시된 필드 검증 포함
+
+## 주요 설계 결정
+
+- `providerSnapshotView`는 pointer가 아닌 value slice (`[]providerSnapshotView`)로 정의하여, 빈 경우 `null`이 아닌 `nil` JSON으로 직렬화되도록 함 (omitempty 태그와 함께)
+- `ServedModels`와 `LifecycleCapabilities`는 `append([]string(nil), ...)`로 복사하여 원본 슬라이스 수정 방지
+- `providerSnapshotViews` helper는 nil 입력 시 nil 반환하여 `json:"provider_snapshots,omitempty"`이 작동하도록 함
+
+## 사용자 리뷰 요청
+
+_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
+
+- 상태: 없음
+- 사유 유형: 없음
+- 연결 대상: 없음
+- 결정 필요: 없음
+- 차단 근거: 없음
+- 실행한 검증/명령: 없음
+- 자동 후속 불가 이유: 없음
+- 재개 조건: 없음
+
+## 리뷰어를 위한 체크포인트
+
+- `edgeStatusResponseViewFromProto`가 `n.GetProviderSnapshots()`를 누락하지 않는지 확인한다.
+- `provider_snapshots` JSON은 read-only status/catalog 필드만 포함하고 Node token/address/raw adapter settings를 노출하지 않는지 확인한다.
+- test fixture가 `load_ratio`, `served_models`, `lifecycle_capabilities`까지 검증하는지 확인한다.
+
+## 검증 결과
+
+_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
+
+필수 규칙:
+- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
+- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
+- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
+- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
+- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
+
+### API-1 중간 검증
+
+```text
+$ go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/Success
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout
+--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus (0.00s)
+ --- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/Success (0.00s)
+ --- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge (0.00s)
+ --- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout (0.00s)
+PASS
+ok iop/apps/control-plane/cmd/control-plane 0.008s
+```
+
+### API-2 중간 검증
+
+```text
+$ go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/Success
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge
+=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout
+--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus (0.00s)
+ --- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/Success (0.00s)
+ --- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge (0.00s)
+ --- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout (0.00s)
+PASS
+ok iop/apps/control-plane/cmd/control-plane 0.008s
+```
+
+### 최종 검증
+
+```text
+$ go test ./apps/control-plane/... -count=1
+ok iop/apps/control-plane/cmd/control-plane 0.143s
+ok iop/apps/control-plane/internal/wire 1.478s
+
+$ git diff --check
+(no output - no whitespace errors)
+```
+
+---
+
+> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
+> If anything is blank, go back and fill it in before saving this file.
+> Leave review-agent-only sections unchanged.
+
+## 섹션 소유권
+
+| Section | Owner | Note |
+|---------|-------|------|
+| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
+| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
+| Spec Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
+| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
+| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
+| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
+| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
+| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected SDD/Milestone lock decision blocks implementation |
+| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
+| 검증 결과 | Implementing agent | Paste actual command output |
+
+## 코드리뷰 결과
+
+- 종합 판정: FAIL
+- 차원별 평가:
+ - Correctness: Fail
+ - Completeness: Fail
+ - Test coverage: Fail
+ - API contract: Fail
+ - Code quality: Pass
+ - Plan deviation: Pass
+ - Verification trust: Pass
+ - Spec conformance: Fail
+- 발견된 문제:
+ - Required: [apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:158)의 `load_ratio,omitempty` 때문에 유효한 idle 상태값인 `load_ratio=0`이 `/edges/{id}/status` JSON에서 누락됩니다. SDD `S07`과 이 plan의 `Spec Targets`는 derived `load_ratio`가 status JSON에 노출되는 것을 요구하므로, `LoadRatio` JSON tag에서 `omitempty`를 제거하고 zero value가 직렬화되는지 보장해야 합니다. [apps/control-plane/cmd/control-plane/main_test.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/main_test.go:638)의 두 번째 provider fixture가 이미 zero-load 케이스지만 raw JSON에서 `load_ratio` 존재 여부를 검증하지 않아 회귀가 잡히지 않습니다. `rawP1["load_ratio"]`가 존재하고 `float64(0)`인지 확인하는 assertion을 추가하세요.
+- 다음 단계:
+ - FAIL 후속 plan/review를 작성한다.
diff --git a/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/complete.log b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/complete.log
new file mode 100644
index 0000000..ea687a4
--- /dev/null
+++ b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/complete.log
@@ -0,0 +1,51 @@
+# Complete - m-provider-catalog-device-status/03_catalog_status_http
+
+## 완료 일시
+
+2026-06-20
+
+## 요약
+
+Control Plane provider status JSON 노출 작업을 2개 루프로 완료했다. 최종 판정: PASS.
+
+## 루프 이력
+
+| Plan | Review | Verdict | 메모 |
+|------|--------|---------|------|
+| `plan_local_G04_0.log` | `code_review_local_G04_0.log` | FAIL | provider snapshot JSON 추가와 테스트 보강은 완료됐으나, `load_ratio=0`이 `omitempty`로 누락되는 API 계약 문제가 발견됐다. |
+| `plan_local_G03_1.log` | `code_review_local_G03_1.log` | PASS | `load_ratio` zero value를 JSON에 보존하고 raw JSON regression assertion을 추가했다. |
+
+## 구현/정리 내용
+
+- Control Plane `/edges/{id}/status` node JSON에 `provider_snapshots`를 추가해 provider health, capacity, in-flight, queued, served models, category, lifecycle capabilities, load ratio를 전달한다.
+- provider snapshot 변환은 proto getter만 사용하고 string slice는 복사해 HTTP DTO로 격리했다.
+- status handler test에 provider snapshot fixture, typed DTO assertion, raw JSON assertion, config/provider snapshot secret non-leak assertion을 추가했다.
+- `load_ratio` JSON tag에서 `omitempty`를 제거해 idle provider의 `load_ratio=0`도 명시적으로 노출되도록 했다.
+
+## 최종 검증
+
+- `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1` - PASS; `ok iop/apps/control-plane/cmd/control-plane 0.005s`
+- `go test ./apps/control-plane/... -count=1` - PASS; `ok iop/apps/control-plane/cmd/control-plane 0.141s`, `ok iop/apps/control-plane/internal/wire 1.487s`
+- `git diff --check` - PASS; no output.
+
+## Roadmap Completion
+
+- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- Completed task ids:
+ - `device-status`: PASS; evidence=`agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G03_1.log`, `agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G03_1.log`; verification=`go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, `git diff --check`
+- Not completed task ids: 없음
+
+## Spec Completion
+
+- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- Completed scenario ids:
+ - `S07`: PASS; task=`device-status`; evidence=`agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G03_1.log`, `agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G03_1.log`; verification=`go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, `git diff --check`
+- Not completed scenario ids: 없음
+
+## 잔여 Nit
+
+- 없음
+
+## 후속 작업
+
+- 없음
diff --git a/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G03_1.log b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G03_1.log
new file mode 100644
index 0000000..f41687b
--- /dev/null
+++ b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G03_1.log
@@ -0,0 +1,97 @@
+
+
+# PLAN REVIEW_API - Preserve Zero Load Ratio In Status JSON
+
+## 이 파일을 읽는 구현 에이전트에게
+
+이 follow-up plan은 직전 코드리뷰의 Required 1건만 해결한다. 구현은 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 코드 변경 후 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다.
+
+구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.
+
+## Roadmap Targets
+
+- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- Task ids:
+ - `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
+- Completion mode: check-on-pass
+
+## Spec Targets
+
+- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- Acceptance scenarios:
+ - `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
+- Completion mode: spec-check-on-pass
+
+## Archive Evidence Snapshot
+
+- Archived plan: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
+- Archived review: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
+- Verdict: FAIL
+- Required summary:
+ - `apps/control-plane/cmd/control-plane/http_views.go:158` uses `json:"load_ratio,omitempty"`, so a valid idle provider with `load_ratio=0` omits the required status field from `/edges/{id}/status` JSON.
+ - `apps/control-plane/cmd/control-plane/main_test.go:638` already has a second provider fixture with implicit zero load ratio, but raw JSON assertions do not verify that `load_ratio` is present and equals `0`.
+- Affected files:
+ - `apps/control-plane/cmd/control-plane/http_views.go`
+ - `apps/control-plane/cmd/control-plane/main_test.go`
+- Prior verification evidence:
+ - Implementer recorded `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` as passing.
+ - Review rerun confirmed `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` pass before this follow-up.
+- Carryover:
+ - Roadmap Targets and Spec Targets remain unchanged.
+ - This follow-up still works toward SDD `S07` and Milestone task `device-status`.
+- Narrow archive reread allowed if needed:
+ - `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
+ - `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
+
+## 분석 결과
+
+### 빌드 등급
+
+- lane/grade: `local-G03`.
+- 근거: 단일 JSON tag 조정과 기존 HTTP handler test assertion 보강이다. 로컬 Go test와 `git diff --check`로 결정적으로 검증 가능하다.
+
+### 범위 결정 근거
+
+- 포함: `LoadRatio` zero value가 Control Plane HTTP JSON에서 보존되도록 DTO tag를 수정한다.
+- 포함: 기존 status handler success fixture의 second provider를 사용해 raw JSON에 `load_ratio: 0`이 존재하는지 확인한다.
+- 제외: provider queue/status 계산, proto 원본/생성물, Client DTO/UI, Edge CLI 표시. 이번 Required와 직접 관련이 없다.
+
+## 구현 체크리스트
+
+- [ ] `apps/control-plane/cmd/control-plane/http_views.go`에서 `providerSnapshotView.LoadRatio` JSON tag를 `json:"load_ratio"`로 바꿔 zero value도 직렬화한다.
+- [ ] `apps/control-plane/cmd/control-plane/main_test.go`에서 second provider raw JSON assertion에 `load_ratio` key 존재 여부와 `float64(0)` 값을 추가한다.
+- [ ] `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`를 실행한다.
+- [ ] `go test ./apps/control-plane/... -count=1`와 `git diff --check`를 실행한다.
+- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
+
+### [REVIEW_API-1] Zero Load Ratio JSON 보존
+
+#### 문제
+
+`ProviderSnapshot.LoadRatio`의 `0`은 idle provider의 정상 상태값이다. 현재 Control Plane HTTP DTO는 `json:"load_ratio,omitempty"`를 사용해 `0`을 JSON에서 생략한다. 이러면 SDD `S07`이 요구한 derived `load_ratio` 노출이 idle provider에서 깨지고, Client는 `0`과 필드 없음/unknown을 구분할 수 없다.
+
+#### 해결 방법
+
+`LoadRatio` JSON tag에서 `omitempty`를 제거한다. 기존 테스트 fixture의 second provider는 `LoadRatio`를 명시하지 않아 zero value 케이스로 쓸 수 있으므로, raw JSON assertion에서 `rawP1["load_ratio"]`가 존재하고 `float64(0)`인지 검증한다.
+
+#### 수정 파일 및 체크리스트
+
+- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: `LoadRatio float32 `json:"load_ratio"``
+- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: `rawP1` assertion에 `load_ratio` 존재/값 검증 추가.
+
+#### 중간 검증
+
+```bash
+go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
+```
+
+기대 결과: status handler success/missing/upstream error subtest가 모두 통과하고, zero `load_ratio` raw JSON assertion도 통과한다.
+
+## 최종 검증
+
+```bash
+go test ./apps/control-plane/... -count=1
+git diff --check
+```
+
+기대 결과: Control Plane Go tests가 통과하고 공백 오류가 없다. Go test cache output은 허용하지 않는다.
diff --git a/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log
new file mode 100644
index 0000000..d58ffc8
--- /dev/null
+++ b/agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log
@@ -0,0 +1,293 @@
+
+
+# PLAN API - Control Plane Provider Status JSON
+
+## 이 파일을 읽는 구현 에이전트에게
+
+이 plan의 구현은 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 코드 변경 후 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.
+
+구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.
+
+## 배경
+
+Edge wire/proto에는 `EdgeNodeSnapshot.provider_snapshots`와 `ProviderSnapshot`의 catalog/status 필드가 이미 있다. 그러나 Control Plane HTTP `/edges/{id}/status` 변환은 node의 config summary만 내보내고 provider snapshots를 버린다. 이 때문에 Edge-owned read-only provider status가 Control Plane과 Client 표면으로 전달되지 않아 `device-status`를 닫을 수 없다.
+
+## 사용자 리뷰 요청 흐름
+
+사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트는 금지하며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
+
+## Roadmap Targets
+
+- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- Task ids:
+ - `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
+- Completion mode: check-on-pass
+
+## Spec Targets
+
+- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- Acceptance scenarios:
+ - `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
+- Completion mode: spec-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/update-roadmap/SKILL.md`
+- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
+- `agent-roadmap/current.md`
+- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
+- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- `agent-ops/rules/project/domain/control-plane/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/control-plane-smoke.md`
+- `agent-test/local/platform-common-smoke.md`
+- `proto/iop/control.proto`
+- `proto/gen/iop/control.pb.go`
+- `proto/iop/runtime.proto`
+- `proto/gen/iop/runtime.pb.go`
+- `apps/control-plane/cmd/control-plane/http_views.go`
+- `apps/control-plane/cmd/control-plane/main_test.go`
+- `apps/edge/internal/controlplane/connector.go`
+- `apps/edge/internal/service/status_provider.go`
+- `apps/edge/internal/service/model_queue.go`
+- `apps/edge/internal/service/status_provider_test.go`
+
+### 테스트 환경 규칙
+
+- test_env: `local`.
+- `agent-test/local/rules.md`를 읽었다. Go quick check는 현재 local checkout에서 실행하고, Docker compose가 필요한 검증은 원격 runner가 필요하다는 규칙을 확인했다.
+- 매칭 profile: `agent-test/local/control-plane-smoke.md`를 읽었다. 적용 명령은 `go test ./apps/control-plane/...`다.
+- 참고 profile: `agent-test/local/platform-common-smoke.md`를 읽었다. 이번 plan은 proto 원본/생성물 변경 없이 기존 generated field를 소비하므로 `make proto`는 요구하지 않는다.
+- HTTP lifecycle 자체를 새 endpoint로 확장하지 않고 기존 `/edges/{id}/status` JSON DTO를 보강한다. `/healthz`, `/readyz`, `make test-control-plane-edge-wire`는 smoke 후보지만 이 plan의 필수 최종 검증은 대상 Go 패키지 테스트로 둔다.
+
+### 테스트 커버리지 공백
+
+- 현재 `TestEdgeRegistryHTTPHandlersGetLiveStatus`는 node config summary와 secret non-leak만 검증하고 provider snapshots JSON을 검증하지 않는다.
+- `edgeStatusResponseViewFromProto`가 `EdgeNodeSnapshot.ProviderSnapshots`를 누락해도 현재 테스트가 실패하지 않는다.
+- Edge internal status coverage는 `apps/edge/internal/service/status_provider_test.go`에서 provider `Capacity`, `InFlight`, `Queued`, `LoadRatio`까지 검증되어 있다. 이번 plan은 Control Plane JSON 변환의 회귀 테스트만 추가한다.
+
+### 심볼 참조
+
+- renamed/removed symbols: none.
+- 새 internal DTO 후보: `providerSnapshotView`.
+- 변경 call site: `edgeStatusResponseViewFromProto` 내부 node mapping only.
+
+### 분할 판단
+
+- split decision policy를 평가했다.
+- 공유 task group: `agent-task/m-provider-catalog-device-status/`.
+- `03_catalog_status_http`: Control Plane HTTP DTO/API test. dependency: none.
+- `04+03_client_catalog_view`: Client DTO/UI 소비. dependency: `03_catalog_status_http` complete.log.
+- 분할 이유: Control Plane JSON 계약이 Client UI의 입력 계약이므로 API surface를 먼저 닫고, Client는 그 JSON shape에 의존해 별도 검증한다. Go HTTP tests와 Flutter widget tests는 서로 다른 runner/리스크를 가진다.
+
+### 범위 결정 근거
+
+- 포함: `apps/control-plane/cmd/control-plane/http_views.go`, `apps/control-plane/cmd/control-plane/main_test.go`.
+- 제외: `proto/iop/*.proto`와 `proto/gen/**`. 이미 `EdgeNodeSnapshot.provider_snapshots`와 `ProviderSnapshot` generated getters가 있으므로 계약 변경이 아니다.
+- 제외: Edge queue/status 계산. `apps/edge/internal/service/model_queue.go`와 status tests가 이미 provider stats를 만든다.
+- 제외: Client DTO/UI. dependent plan `04+03_client_catalog_view`에서 처리한다.
+- 제외: runtime control, resource telemetry, qualification report. SDD D05-D06과 Milestone 범위 제외에 따라 후속 범위다.
+
+### 빌드 등급
+
+- lane/grade: `local-G04`.
+- 근거: 단일 Go app HTTP DTO 변환과 대상 테스트 보강이다. 기존 proto/generated 타입이 있어 public wire contract 변경은 없고, `go test ./apps/control-plane/...`로 결정적으로 검증 가능하다.
+
+## 구현 체크리스트
+
+- [ ] Control Plane HTTP status DTO에 provider snapshot read-only fields를 추가하고 `EdgeNodeSnapshot.ProviderSnapshots`를 JSON으로 전달한다.
+- [ ] `/edges/{id}/status` 테스트에 provider snapshot fixture와 JSON assertion을 추가해 `health`, `capacity`, `in_flight`, `load_ratio`, `served_models`, `queued`, `category`, `lifecycle_capabilities`가 보존되는지 확인한다.
+- [ ] raw config secret non-leak 검증이 provider snapshot 추가 후에도 유지되는지 확인한다.
+- [ ] `go test ./apps/control-plane/...`를 실행한다.
+- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
+
+### [API-1] Provider Snapshot JSON DTO 추가
+
+#### 문제
+
+[apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:138)에서 `edgeNodeSnapshotView`가 `NodeID`, `Alias`, `Label`, `Connected`, `Config`만 가진다. [apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:191)의 `edgeStatusResponseViewFromProto`도 `n.GetProviderSnapshots()`를 읽지 않아 Edge가 보낸 provider status가 HTTP JSON에서 사라진다.
+
+현재 코드:
+
+```go
+138 type edgeNodeSnapshotView struct {
+139 NodeID string `json:"node_id"`
+140 Alias string `json:"alias"`
+141 Label string `json:"label"`
+142 Connected bool `json:"connected"`
+143 Config *nodeConfigSummaryView `json:"config,omitempty"`
+144 }
+...
+191 func edgeStatusResponseViewFromProto(resp *iop.EdgeStatusResponse) edgeStatusResponseView {
+192 nodes := make([]edgeNodeSnapshotView, 0, len(resp.GetNodes()))
+193 for _, n := range resp.GetNodes() {
+194 nodes = append(nodes, edgeNodeSnapshotView{
+195 NodeID: n.GetNodeId(),
+196 Alias: n.GetAlias(),
+197 Label: n.GetLabel(),
+198 Connected: n.GetConnected(),
+199 Config: newNodeConfigSummaryView(n.GetConfig()),
+200 })
+201 }
+```
+
+#### 해결 방법
+
+`providerSnapshotView`를 `http_views.go`에 추가하고 `edgeNodeSnapshotView`에 `ProviderSnapshots []providerSnapshotView 'json:"provider_snapshots,omitempty"'`를 둔다. 변환 helper는 proto getters만 사용하고 nil/empty는 nil을 반환한다.
+
+목표 코드:
+
+```go
+type providerSnapshotView struct {
+ Adapter string `json:"adapter,omitempty"`
+ Status string `json:"status,omitempty"`
+ Capacity int32 `json:"capacity"`
+ InFlight int32 `json:"in_flight"`
+ Queued int32 `json:"queued"`
+ ID string `json:"id,omitempty"`
+ Type string `json:"type,omitempty"`
+ Category string `json:"category,omitempty"`
+ ServedModels []string `json:"served_models,omitempty"`
+ Health string `json:"health,omitempty"`
+ LoadRatio float32 `json:"load_ratio,omitempty"`
+ LifecycleCapabilities []string `json:"lifecycle_capabilities,omitempty"`
+}
+
+func providerSnapshotViews(snaps []*iop.ProviderSnapshot) []providerSnapshotView {
+ if len(snaps) == 0 {
+ return nil
+ }
+ views := make([]providerSnapshotView, 0, len(snaps))
+ for _, p := range snaps {
+ views = append(views, providerSnapshotView{
+ Adapter: p.GetAdapter(),
+ Status: p.GetStatus(),
+ Capacity: p.GetCapacity(),
+ InFlight: p.GetInFlight(),
+ Queued: p.GetQueued(),
+ ID: p.GetId(),
+ Type: p.GetType(),
+ Category: p.GetCategory(),
+ ServedModels: append([]string(nil), p.GetServedModels()...),
+ Health: p.GetHealth(),
+ LoadRatio: p.GetLoadRatio(),
+ LifecycleCapabilities: append([]string(nil), p.GetLifecycleCapabilities()...),
+ })
+ }
+ return views
+}
+```
+
+`edgeStatusResponseViewFromProto`의 node append에는 `ProviderSnapshots: providerSnapshotViews(n.GetProviderSnapshots())`를 추가한다.
+
+#### 수정 파일 및 체크리스트
+
+- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: DTO struct와 helper 추가.
+- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: `edgeStatusResponseViewFromProto` node mapping에 provider snapshots 추가.
+- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: JSON shape assertion 추가.
+
+#### 테스트 작성
+
+- 작성: `apps/control-plane/cmd/control-plane/main_test.go`의 `TestEdgeRegistryHTTPHandlersGetLiveStatus/Success`에 provider snapshot fixture와 typed/raw assertion을 추가한다.
+
+#### 중간 검증
+
+```bash
+go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
+```
+
+기대 결과: provider snapshot JSON assertion과 기존 secret non-leak assertion이 모두 통과한다.
+
+### [API-2] Status JSON 회귀 테스트 보강
+
+#### 문제
+
+[apps/control-plane/cmd/control-plane/main_test.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/main_test.go:444)의 fixture는 `EdgeStatusResponse.Nodes[0]`에 config만 넣고 provider snapshots를 넣지 않는다. 그래서 `provider_snapshots` 누락이 회귀로 잡히지 않는다.
+
+현재 fixture 일부:
+
+```go
+444 return &iop.EdgeStatusResponse{
+445 RequestId: "req-123",
+446 EdgeId: "edge-a",
+447 EdgeName: "Edge A",
+448 ObservedTimeUnixNano: connectedAt.UnixNano(),
+449 Nodes: []*iop.EdgeNodeSnapshot{
+450 {
+451 NodeId: "node-1",
+452 Alias: "alpha",
+453 Label: "node0",
+454 Connected: true,
+455 Config: &iop.NodeConfigPayload{
+```
+
+#### 해결 방법
+
+fixture에 provider snapshot을 추가하고 typed DTO와 raw JSON 모두 확인한다. raw assertion은 `provider_snapshots` 키가 있고 secret/config internals가 들어가지 않는다는 기존 검증과 공존해야 한다.
+
+목표 fixture:
+
+```go
+ProviderSnapshots: []*iop.ProviderSnapshot{
+ {
+ Adapter: "vllm-gpu",
+ Status: "available",
+ Capacity: 4,
+ InFlight: 1,
+ Queued: 2,
+ Id: "provider-vllm-primary",
+ Type: "vllm",
+ Category: "api",
+ ServedModels: []string{"nvidia/Qwen3.6-35B-A3B-NVFP4"},
+ Health: "available",
+ LoadRatio: 0.25,
+ LifecycleCapabilities: []string{"list_models", "load_model"},
+ },
+},
+```
+
+Assert `node.ProviderSnapshots[0]` preserves all fields, and raw JSON has `provider_snapshots[0].load_ratio == 0.25`, `served_models[0]`, `lifecycle_capabilities[0]`.
+
+#### 수정 파일 및 체크리스트
+
+- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: fixture에 provider snapshot 추가.
+- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: typed DTO assertion 추가.
+- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: raw JSON key assertion 추가.
+
+#### 테스트 작성
+
+- 작성: 위 test에 직접 추가한다. 별도 새 test보다 기존 status handler 성공 케이스에 붙이는 편이 HTTP view 계약을 한 곳에서 검증한다.
+
+#### 중간 검증
+
+```bash
+go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
+```
+
+기대 결과: status handler 성공/누락/502 subtest가 모두 통과한다.
+
+## 수정 파일 요약
+
+| 파일 | 항목 |
+|------|------|
+| `apps/control-plane/cmd/control-plane/http_views.go` | API-1 |
+| `apps/control-plane/cmd/control-plane/main_test.go` | API-2 |
+
+## 최종 검증
+
+```bash
+go test ./apps/control-plane/... -count=1
+git diff --check
+```
+
+기대 결과: Go control-plane tests가 통과하고 공백 오류가 없다. Go test cache output은 허용하지 않는다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
diff --git a/agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/CODE_REVIEW-local-G05.md b/agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/CODE_REVIEW-local-G05.md
new file mode 100644
index 0000000..f932f8b
--- /dev/null
+++ b/agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/CODE_REVIEW-local-G05.md
@@ -0,0 +1,172 @@
+
+
+# Code Review Reference - API
+
+> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
+> The task is NOT complete until every implementation-owned section below is filled in.
+> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
+> Fill implementation-owned sections, then stop with active files in place and report ready for review.
+> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
+> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review.
+> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
+> Follow the ownership table at the bottom of this file for which sections you own.
+
+## 개요
+
+date=2026-06-20
+task=m-provider-catalog-device-status/04+03_client_catalog_view, plan=0, tag=API
+
+## Roadmap Targets
+
+- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- Task ids:
+ - `ops-review`: Edge config와 Edge-owned read-only catalog/status를 기준으로 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있다.
+- Completion mode: check-on-pass
+
+## Spec Targets
+
+- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- Acceptance scenarios:
+ - `S10`: task=`ops-review`; evidence=`Edge CLI configured-provider summary, Control Plane provider status JSON, Client Nodes panel provider catalog/status rendering, and resolved user_review_0.log are present.`
+- Completion mode: spec-check-on-pass
+
+## 이 파일을 읽는 리뷰 에이전트에게
+
+> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
+
+각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
+리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
+
+1. 판정을 append한다.
+2. `CODE_REVIEW-local-G05.md` -> `code_review_local_G05_0.log`, `PLAN-local-G05.md` -> `plan_local_G05_0.log`로 아카이브한다.
+3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/04+03_client_catalog_view/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
+4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
+5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
+
+---
+
+## 구현 항목별 완료 여부
+
+| 항목 | 완료 여부 |
+|------|---------|
+| [API-1] Client Provider Snapshot DTO 추가 | [ ] |
+| [API-2] Nodes Panel Provider Catalog 표시 | [ ] |
+| [API-3] Ops Review Evidence 연결 | [ ] |
+
+## 구현 체크리스트
+
+- [ ] 선행 task `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log` 또는 matching archive complete.log가 존재하는지 확인하고, 없으면 구현하지 말고 dependency missing으로 기록한다.
+- [ ] Client HTTP DTO에 `ProviderSnapshotView`와 `EdgeNodeSnapshotView.providerSnapshots` parsing을 추가한다.
+- [ ] Nodes panel에 read-only Provider Catalog/Status 섹션을 추가해 provider id/type/category, health/status, capacity, in-flight, queued, load ratio, served models, lifecycle capabilities를 표시한다.
+- [ ] Fake status repository와 widget test fixture에 provider snapshot을 추가하고 Nodes panel test가 provider catalog/status 표시를 검증하도록 보강한다.
+- [ ] `cd apps/client && flutter test`를 실행한다.
+- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
+
+## 코드리뷰 전용 체크리스트
+
+> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
+> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
+
+- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
+- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
+- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G05_0.log`로 아카이브한다.
+- [ ] active `PLAN-*-G??.md`를 `plan_local_G05_0.log`로 아카이브한다.
+- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
+- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
+- [ ] PASS이면 active task 디렉터리 `agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/`를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/04+03_client_catalog_view/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
+- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
+- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-provider-catalog-device-status/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
+- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G05.md`와 `CODE_REVIEW-local-G05.md`를 작성하고 `complete.log`를 작성하지 않는다.
+- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
+- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
+
+## 계획 대비 변경 사항
+
+_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
+
+## 주요 설계 결정
+
+_구현 에이전트가 주요 설계 결정 사항을 기록한다._
+
+## 사용자 리뷰 요청
+
+_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
+
+- 상태: 없음
+- 사유 유형: 없음
+- 연결 대상: 없음
+- 결정 필요: 없음
+- 차단 근거: 없음
+- 실행한 검증/명령: 없음
+- 자동 후속 불가 이유: 없음
+- 재개 조건: 없음
+
+## 리뷰어를 위한 체크포인트
+
+- 구현 전 `03_catalog_status_http` complete.log가 확인되었는지 본다.
+- DTO parsing이 `capacity`, `in_flight`, `queued`, `load_ratio` numeric 값을 안전하게 처리하는지 확인한다.
+- Nodes panel이 provider catalog/status를 표시하되 runtime control UI를 추가하지 않았는지 확인한다.
+- `ops-review` evidence에 Edge CLI, Control Plane, Client, `user_review_0.log` 연결이 남았는지 확인한다.
+
+## 검증 결과
+
+_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
+
+필수 규칙:
+- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
+- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
+- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
+- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
+- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
+
+### API-1 중간 검증
+
+```text
+$ cd apps/client && flutter test test/widget_test.dart
+(output)
+```
+
+### API-2 중간 검증
+
+```text
+$ cd apps/client && flutter test test/widget_test.dart
+(output)
+```
+
+### API-3 중간 검증
+
+```text
+$ test -f agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log || find agent-task/archive -path '*/m-provider-catalog-device-status/03_catalog_status_http/complete.log' -print -quit
+(output)
+```
+
+### 최종 검증
+
+```text
+$ cd apps/client && flutter test
+(output)
+
+$ git diff --check
+(output)
+```
+
+---
+
+> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
+> If anything is blank, go back and fill it in before saving this file.
+> Leave review-agent-only sections unchanged.
+
+## 섹션 소유권
+
+| Section | Owner | Note |
+|---------|-------|------|
+| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
+| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
+| Spec Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
+| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
+| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
+| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
+| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
+| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected SDD/Milestone lock decision blocks implementation |
+| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
+| 검증 결과 | Implementing agent | Paste actual command output |
diff --git a/agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/PLAN-local-G05.md b/agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/PLAN-local-G05.md
new file mode 100644
index 0000000..50767b2
--- /dev/null
+++ b/agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/PLAN-local-G05.md
@@ -0,0 +1,335 @@
+
+
+# PLAN API - Client Provider Catalog View
+
+## 이 파일을 읽는 구현 에이전트에게
+
+이 plan은 `04+03_client_catalog_view` dependent split task다. 같은 task group의 `03_catalog_status_http`가 `complete.log`를 만들기 전에는 구현을 시작하지 않는다. 구현이 끝나도 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 검증 명령의 실제 stdout/stderr를 review stub에 붙이고 active 파일을 그대로 둔 뒤 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.
+
+구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.
+
+## 배경
+
+Control Plane Client는 `/edges/{id}/status` JSON을 소비하지만 `EdgeNodeSnapshotView`가 provider snapshots를 파싱하지 않는다. Nodes panel도 node config adapters만 보여 주므로 운영자가 provider category, status, load, served models, lifecycle capability를 Client에서 확인할 수 없다. 03번 plan이 Control Plane HTTP JSON 계약을 닫으면, 이 plan은 Client DTO와 Nodes view에서 같은 read-only catalog/status를 표시한다.
+
+## 사용자 리뷰 요청 흐름
+
+사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트는 금지하며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
+
+## Roadmap Targets
+
+- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- Task ids:
+ - `ops-review`: Edge config와 Edge-owned read-only catalog/status를 기준으로 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있다.
+- Completion mode: check-on-pass
+
+## Spec Targets
+
+- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- Acceptance scenarios:
+ - `S10`: task=`ops-review`; evidence=`Edge CLI configured-provider summary, Control Plane provider status JSON, Client Nodes panel provider catalog/status rendering, and resolved user_review_0.log are present.`
+- Completion mode: spec-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/update-roadmap/SKILL.md`
+- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
+- `agent-roadmap/current.md`
+- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
+- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
+- `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
+- `agent-ops/rules/project/domain/client/rules.md`
+- `agent-ops/rules/project/domain/control-plane/rules.md`
+- `agent-ops/rules/project/domain/testing/rules.md`
+- `agent-test/local/rules.md`
+- `agent-test/local/client-smoke.md`
+- `agent-test/local/control-plane-smoke.md`
+- `apps/client/lib/control_plane_status_client.dart`
+- `apps/client/lib/control_plane_status_dto.dart`
+- `apps/client/lib/control_plane_status_repository.dart`
+- `apps/client/lib/widgets/nodes_panel.dart`
+- `apps/client/lib/widgets/edges_panel.dart`
+- `apps/client/test/widget_test.dart`
+- `apps/client/test/control_plane_status_controller_test.dart`
+- `apps/control-plane/cmd/control-plane/http_views.go`
+- `proto/iop/control.proto`
+- `proto/iop/runtime.proto`
+
+### 테스트 환경 규칙
+
+- test_env: `local`.
+- `agent-test/local/rules.md`를 읽었다. Flutter client 포함 검증은 원격 runner 또는 동기화된 Flutter 환경 기준이며, 현재 checkout의 변경분이 원격에 없으면 원격 검증 완료로 보지 않는다고 확인했다.
+- 매칭 profile: `agent-test/local/client-smoke.md`를 읽었다. 필수 명령은 `cd apps/client && flutter test`다.
+- 관련 upstream profile: `agent-test/local/control-plane-smoke.md`를 읽었다. 이 plan은 Control Plane 코드를 수정하지 않으며 03번 plan의 complete.log를 선행 조건으로 둔다.
+- Docker compose, web dev server, analyzer는 이번 DTO/widget 범위의 필수 검증이 아니다. Flutter analyzer 영향이 확인되면 구현 에이전트가 `cd apps/client && flutter analyze --no-fatal-infos`를 추가 실행하고 review stub에 기록한다.
+
+### 테스트 커버리지 공백
+
+- `EdgeNodeSnapshotView.fromJson`은 `provider_snapshots`를 파싱하지 않는다. HTTP repository가 JSON을 받아도 DTO에서 버린다.
+- `NodesPanel`은 node config adapters만 표시한다. provider status, category, served models, load ratio, lifecycle capabilities가 UI에 없다.
+- `FakeControlPlaneStatusRepository.fetchEdgeStatus` fixture와 `Client App opens Nodes panel...` widget test는 provider catalog/status가 표시되어야 한다는 회귀 조건을 갖고 있지 않다.
+
+### 심볼 참조
+
+- renamed/removed symbols: none.
+- 새 DTO 후보: `ProviderSnapshotView`.
+- 새 field 후보: `EdgeNodeSnapshotView.providerSnapshots`.
+- 변경 call sites: `EdgeNodeSnapshotView` constructor usages in `apps/client/test/widget_test.dart` and `apps/client/test/control_plane_status_controller_test.dart` may need `providerSnapshots: []` only if constructor is kept `required`; prefer optional default list to minimize call-site churn.
+
+### 분할 판단
+
+- split decision policy를 평가했다.
+- 공유 task group: `agent-task/m-provider-catalog-device-status/`.
+- predecessor: `03_catalog_status_http` - 현재 active plan으로 존재하며 complete.log는 아직 없음.
+- current task: `04+03_client_catalog_view` - `03` complete.log가 필요하다.
+- 분할 이유: Client DTO/UI는 Control Plane HTTP JSON shape에 의존한다. Go API 변환과 Flutter widget 검증은 서로 다른 ownership/test runner를 가지므로 별도 review unit으로 둔다.
+
+### 범위 결정 근거
+
+- 포함: `apps/client/lib/control_plane_status_dto.dart`, `apps/client/lib/widgets/nodes_panel.dart`, `apps/client/test/widget_test.dart`. 필요 시 DTO parse 단위 test를 기존 client test 파일에 추가할 수 있다.
+- 제외: `apps/control-plane/**`. 03번 plan의 PASS가 선행 조건이다.
+- 제외: generated Dart proto. Client는 Control Plane HTTP JSON DTO를 소비하므로 proto binding 수정이 아니다.
+- 제외: Edge CLI. 작은 직접 작업으로 `iop-edge nodes list` configured provider summary가 추가되어 있다.
+- 제외: runtime enable/disable, drain, fallback 우선순위, capacity override. SDD D06과 Milestone 범위 제외에 따라 후속 범위다.
+
+### 빌드 등급
+
+- lane/grade: `local-G05`.
+- 근거: Flutter DTO와 widget 표시 변경이며 UI test로 검증 가능하다. 다만 Client runner/Flutter SDK 상태와 layout 회귀 위험이 있어 G04보다 한 단계 높였다.
+
+## 구현 체크리스트
+
+- [ ] 선행 task `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log` 또는 matching archive complete.log가 존재하는지 확인하고, 없으면 구현하지 말고 dependency missing으로 기록한다.
+- [ ] Client HTTP DTO에 `ProviderSnapshotView`와 `EdgeNodeSnapshotView.providerSnapshots` parsing을 추가한다.
+- [ ] Nodes panel에 read-only Provider Catalog/Status 섹션을 추가해 provider id/type/category, health/status, capacity, in-flight, queued, load ratio, served models, lifecycle capabilities를 표시한다.
+- [ ] Fake status repository와 widget test fixture에 provider snapshot을 추가하고 Nodes panel test가 provider catalog/status 표시를 검증하도록 보강한다.
+- [ ] `cd apps/client && flutter test`를 실행한다.
+- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
+
+## 의존 관계 및 구현 순서
+
+- 이 subtask dir 이름 `04+03_client_catalog_view`는 predecessor index `03`만 요구한다.
+- 구현 시작 전 다음 중 하나를 확인한다.
+ - active: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log`
+ - archive: `agent-task/archive/*/*/m-provider-catalog-device-status/03_catalog_status_http/complete.log`
+- 현재 계획 작성 시점에는 predecessor complete.log가 없다. 구현 에이전트는 03 PASS 이후 진행한다.
+
+### [API-1] Client Provider Snapshot DTO 추가
+
+#### 문제
+
+[apps/client/lib/control_plane_status_dto.dart](/config/workspace/iop/apps/client/lib/control_plane_status_dto.dart:65)의 `EdgeNodeSnapshotView`는 provider snapshots field가 없다. [apps/client/lib/control_plane_status_dto.dart](/config/workspace/iop/apps/client/lib/control_plane_status_dto.dart:80)의 `fromJson`도 `provider_snapshots`를 읽지 않는다.
+
+현재 코드:
+
+```dart
+65 class EdgeNodeSnapshotView {
+66 final String nodeId;
+67 final String alias;
+68 final String label;
+69 final bool connected;
+70 final NodeConfigSummaryView? config;
+...
+80 factory EdgeNodeSnapshotView.fromJson(Map json) {
+81 return EdgeNodeSnapshotView(
+82 nodeId: json['node_id'] as String? ?? '',
+83 alias: json['alias'] as String? ?? '',
+84 label: json['label'] as String? ?? '',
+85 connected: json['connected'] as bool? ?? false,
+86 config: json['config'] != null
+87 ? NodeConfigSummaryView.fromJson(json['config'] as Map)
+88 : null,
+89 );
+90 }
+91 }
+```
+
+#### 해결 방법
+
+`ProviderSnapshotView` DTO를 추가한다. Numeric JSON은 Go에서 `int`/`float`로 올 수 있으므로 `capacity`, `in_flight`, `queued`는 `as num?` 후 `toInt()`, `load_ratio`는 `toDouble()`로 파싱한다. `EdgeNodeSnapshotView` 생성자에는 `List providerSnapshots = const []` default를 둬 기존 fixture churn을 줄인다.
+
+목표 코드:
+
+```dart
+class ProviderSnapshotView {
+ final String id;
+ final String adapter;
+ final String type;
+ final String category;
+ final String status;
+ final String health;
+ final int capacity;
+ final int inFlight;
+ final int queued;
+ final double loadRatio;
+ final List servedModels;
+ final List lifecycleCapabilities;
+
+ ProviderSnapshotView({
+ required this.id,
+ required this.adapter,
+ required this.type,
+ required this.category,
+ required this.status,
+ required this.health,
+ required this.capacity,
+ required this.inFlight,
+ required this.queued,
+ required this.loadRatio,
+ required this.servedModels,
+ required this.lifecycleCapabilities,
+ });
+
+ factory ProviderSnapshotView.fromJson(Map json) {
+ return ProviderSnapshotView(
+ id: json['id'] as String? ?? '',
+ adapter: json['adapter'] as String? ?? '',
+ type: json['type'] as String? ?? '',
+ category: json['category'] as String? ?? '',
+ status: json['status'] as String? ?? '',
+ health: json['health'] as String? ?? '',
+ capacity: (json['capacity'] as num?)?.toInt() ?? 0,
+ inFlight: (json['in_flight'] as num?)?.toInt() ?? 0,
+ queued: (json['queued'] as num?)?.toInt() ?? 0,
+ loadRatio: (json['load_ratio'] as num?)?.toDouble() ?? 0,
+ servedModels: List.from(json['served_models'] ?? []),
+ lifecycleCapabilities: List.from(json['lifecycle_capabilities'] ?? []),
+ );
+ }
+}
+```
+
+#### 수정 파일 및 체크리스트
+
+- [ ] `apps/client/lib/control_plane_status_dto.dart`: DTO 추가.
+- [ ] `apps/client/lib/control_plane_status_dto.dart`: `EdgeNodeSnapshotView.providerSnapshots` 추가 및 JSON parsing.
+- [ ] `apps/client/test/widget_test.dart` 또는 기존 client unit test: JSON parse assertion 추가 또는 widget fixture에서 파싱 경로 검증.
+
+#### 테스트 작성
+
+- 작성: `apps/client/test/widget_test.dart`의 fake repository fixture에 provider snapshots를 추가한다. 별도 pure DTO test가 더 간결하면 같은 test file 또는 `control_plane_status_controller_test.dart`에 추가해도 된다.
+
+#### 중간 검증
+
+```bash
+cd apps/client && flutter test test/widget_test.dart
+```
+
+기대 결과: 기존 app/widget tests와 provider DTO 표시 assertion이 통과한다.
+
+### [API-2] Nodes Panel Provider Catalog 표시
+
+#### 문제
+
+[apps/client/lib/widgets/nodes_panel.dart](/config/workspace/iop/apps/client/lib/widgets/nodes_panel.dart:211)은 config가 있을 때 concurrency와 adapters만 표시한다. provider snapshots가 추가되어도 UI에서 볼 수 없다.
+
+현재 코드:
+
+```dart
+211 if (node.config != null) ...[
+212 const SizedBox(height: 16),
+213 const Divider(color: Color(0xFF334155)),
+214 const SizedBox(height: 12),
+215 Row(
+216 children: [
+217 const Icon(Icons.bolt, size: 16, color: Color(0xFF6366F1)),
+218 const SizedBox(width: 6),
+219 Text(
+220 'Concurrency Limit: ${node.config!.concurrency}',
+...
+226 const Text(
+227 'Adapters',
+...
+234 Wrap(
+235 spacing: 8,
+236 runSpacing: 8,
+237 children: node.config!.adapters.map((adapter) {
+```
+
+#### 해결 방법
+
+Node card 안에 `Provider Catalog` 섹션을 추가한다. Cards 안에 별도 `Card`를 중첩하지 말고 기존 dark panel 안에서 `Column`, `Wrap`, `Container`로 compact rows를 만든다. 표시 필드는 다음을 포함한다.
+
+- provider label: `id`가 있으면 id, 없으면 adapter.
+- type/category: `type`과 `category`.
+- state: `health` 우선, 없으면 `status`.
+- load: `inFlight/capacity`, `queued`, `loadRatio`.
+- served models: comma-separated first line, 길면 wrap.
+- lifecycle capabilities: coarse flags만 표시.
+
+UI text는 기존 Nodes panel 스타일에 맞춰 작고 조밀하게 둔다. 새 marketing/설명 문구는 추가하지 않는다.
+
+#### 수정 파일 및 체크리스트
+
+- [ ] `apps/client/lib/widgets/nodes_panel.dart`: provider section helper 추가.
+- [ ] `apps/client/lib/widgets/nodes_panel.dart`: node card에 provider snapshots가 있을 때만 section 렌더링.
+- [ ] `apps/client/test/widget_test.dart`: `Provider Catalog`, provider id/type/category/status/load/models/lifecycle text assertion 추가.
+
+#### 테스트 작성
+
+- 작성: `Client App opens Nodes panel and displays active Nodes and configurations`에 provider display assertion을 추가한다.
+
+#### 중간 검증
+
+```bash
+cd apps/client && flutter test test/widget_test.dart
+```
+
+기대 결과: Nodes panel test가 provider catalog/status 표시까지 검증한다.
+
+### [API-3] Ops Review Evidence 연결
+
+#### 문제
+
+Milestone `ops-review`는 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있어야 한다. 작은 직접 작업으로 `iop-edge nodes list` configured provider summary는 추가됐지만, Client plan이 PASS될 때 전체 소비 경로 근거가 review/complete log에 남아야 한다.
+
+#### 해결 방법
+
+review stub의 `주요 설계 결정` 또는 `검증 결과`에 다음 증거를 남긴다.
+
+- Edge CLI: `apps/edge/internal/edgecmd/nodes.go`가 configured provider category/type/models/capacity/lifecycle summary를 출력하고 `TestNodesListPrintsConfiguredProviders`가 통과했다.
+- Control Plane: predecessor 03 complete.log가 `/edges/{id}/status` provider snapshots JSON을 검증했다.
+- Client: 이 plan의 widget test가 provider catalog/status 표시를 검증했다.
+- SDD review: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/user_review_0.log`가 D01-D06 해결 이력으로 존재한다.
+
+#### 수정 파일 및 체크리스트
+
+- [ ] `CODE_REVIEW-local-G05.md`: 구현 후 주요 설계 결정 또는 검증 결과에 위 evidence 연결을 기록한다.
+- [ ] source code는 API-1/API-2 범위를 넘기지 않는다.
+
+#### 테스트 작성
+
+- 별도 테스트 없음. API-1/API-2 테스트와 predecessor 03 complete.log가 evidence다.
+
+#### 중간 검증
+
+```bash
+test -f agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log || find agent-task/archive -path '*/m-provider-catalog-device-status/03_catalog_status_http/complete.log' -print -quit
+```
+
+기대 결과: 03 predecessor complete.log 경로가 출력된다.
+
+## 수정 파일 요약
+
+| 파일 | 항목 |
+|------|------|
+| `apps/client/lib/control_plane_status_dto.dart` | API-1 |
+| `apps/client/lib/widgets/nodes_panel.dart` | API-2 |
+| `apps/client/test/widget_test.dart` | API-1, API-2 |
+| `agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/CODE_REVIEW-local-G05.md` | API-3 |
+
+## 최종 검증
+
+```bash
+cd apps/client && flutter test
+git diff --check
+```
+
+기대 결과: Flutter tests가 통과하고 공백 오류가 없다. Go test cache output은 해당 없음. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
diff --git a/apps/control-plane/cmd/control-plane/http_views.go b/apps/control-plane/cmd/control-plane/http_views.go
index 19ee0e1..ed892f6 100644
--- a/apps/control-plane/cmd/control-plane/http_views.go
+++ b/apps/control-plane/cmd/control-plane/http_views.go
@@ -136,11 +136,27 @@ type fleetCommandResponse struct {
}
type edgeNodeSnapshotView struct {
- NodeID string `json:"node_id"`
- Alias string `json:"alias"`
- Label string `json:"label"`
- Connected bool `json:"connected"`
- Config *nodeConfigSummaryView `json:"config,omitempty"`
+ NodeID string `json:"node_id"`
+ Alias string `json:"alias"`
+ Label string `json:"label"`
+ Connected bool `json:"connected"`
+ Config *nodeConfigSummaryView `json:"config,omitempty"`
+ ProviderSnapshots []providerSnapshotView `json:"provider_snapshots,omitempty"`
+}
+
+type providerSnapshotView struct {
+ Adapter string `json:"adapter,omitempty"`
+ Status string `json:"status,omitempty"`
+ Capacity int32 `json:"capacity"`
+ InFlight int32 `json:"in_flight"`
+ Queued int32 `json:"queued"`
+ ID string `json:"id,omitempty"`
+ Type string `json:"type,omitempty"`
+ Category string `json:"category,omitempty"`
+ ServedModels []string `json:"served_models,omitempty"`
+ Health string `json:"health,omitempty"`
+ LoadRatio float32 `json:"load_ratio"`
+ LifecycleCapabilities []string `json:"lifecycle_capabilities,omitempty"`
}
type nodeConfigSummaryView struct {
@@ -176,6 +192,30 @@ func newNodeConfigSummaryView(payload *iop.NodeConfigPayload) *nodeConfigSummary
}
}
+func providerSnapshotViews(snaps []*iop.ProviderSnapshot) []providerSnapshotView {
+ if len(snaps) == 0 {
+ return nil
+ }
+ views := make([]providerSnapshotView, 0, len(snaps))
+ for _, p := range snaps {
+ views = append(views, providerSnapshotView{
+ Adapter: p.GetAdapter(),
+ Status: p.GetStatus(),
+ Capacity: p.GetCapacity(),
+ InFlight: p.GetInFlight(),
+ Queued: p.GetQueued(),
+ ID: p.GetId(),
+ Type: p.GetType(),
+ Category: p.GetCategory(),
+ ServedModels: append([]string(nil), p.GetServedModels()...),
+ Health: p.GetHealth(),
+ LoadRatio: p.GetLoadRatio(),
+ LifecycleCapabilities: append([]string(nil), p.GetLifecycleCapabilities()...),
+ })
+ }
+ return views
+}
+
func edgeRegistryViewFromState(state wire.EdgeConnectionState) edgeRegistryView {
return edgeRegistryView{
EdgeID: state.EdgeID,
@@ -192,11 +232,12 @@ func edgeStatusResponseViewFromProto(resp *iop.EdgeStatusResponse) edgeStatusRes
nodes := make([]edgeNodeSnapshotView, 0, len(resp.GetNodes()))
for _, n := range resp.GetNodes() {
nodes = append(nodes, edgeNodeSnapshotView{
- NodeID: n.GetNodeId(),
- Alias: n.GetAlias(),
- Label: n.GetLabel(),
- Connected: n.GetConnected(),
- Config: newNodeConfigSummaryView(n.GetConfig()),
+ NodeID: n.GetNodeId(),
+ Alias: n.GetAlias(),
+ Label: n.GetLabel(),
+ Connected: n.GetConnected(),
+ Config: newNodeConfigSummaryView(n.GetConfig()),
+ ProviderSnapshots: providerSnapshotViews(n.GetProviderSnapshots()),
})
}
return edgeStatusResponseView{
diff --git a/apps/control-plane/cmd/control-plane/main_test.go b/apps/control-plane/cmd/control-plane/main_test.go
index b6e6f21..f9614ea 100644
--- a/apps/control-plane/cmd/control-plane/main_test.go
+++ b/apps/control-plane/cmd/control-plane/main_test.go
@@ -464,6 +464,32 @@ func TestEdgeRegistryHTTPHandlersGetLiveStatus(t *testing.T) {
Concurrency: 4,
},
},
+ ProviderSnapshots: []*iop.ProviderSnapshot{
+ {
+ Adapter: "vllm-gpu",
+ Status: "available",
+ Capacity: 4,
+ InFlight: 1,
+ Queued: 2,
+ Id: "provider-vllm-primary",
+ Type: "vllm",
+ Category: "api",
+ ServedModels: []string{"nvidia/Qwen3.6-35B-A3B-NVFP4"},
+ Health: "available",
+ LoadRatio: 0.25,
+ LifecycleCapabilities: []string{"list_models", "load_model"},
+ },
+ {
+ Adapter: "ollama-cpu",
+ Status: "backlog",
+ Capacity: 2,
+ InFlight: 2,
+ Queued: 5,
+ Id: "provider-ollama-default",
+ Type: "ollama",
+ Health: "unavailable",
+ },
+ },
},
},
}, nil
@@ -505,6 +531,41 @@ func TestEdgeRegistryHTTPHandlersGetLiveStatus(t *testing.T) {
t.Fatalf("unexpected adapter summary: %+v", adapter)
}
+ // Provider snapshots assertions
+ if len(node.ProviderSnapshots) != 2 {
+ t.Fatalf("expected 2 provider snapshots, got %d", len(node.ProviderSnapshots))
+ }
+
+ // First provider snapshot (vllm-gpu)
+ p0 := node.ProviderSnapshots[0]
+ if p0.Adapter != "vllm-gpu" || p0.Status != "available" || p0.Capacity != 4 {
+ t.Fatalf("unexpected provider snapshot[0]: %+v", p0)
+ }
+ if p0.InFlight != 1 || p0.Queued != 2 {
+ t.Fatalf("unexpected in_flight/queued: in_flight=%d queued=%d", p0.InFlight, p0.Queued)
+ }
+ if p0.ID != "provider-vllm-primary" || p0.Type != "vllm" || p0.Category != "api" {
+ t.Fatalf("unexpected id/type/category: id=%q type=%q category=%q", p0.ID, p0.Type, p0.Category)
+ }
+ if len(p0.ServedModels) != 1 || p0.ServedModels[0] != "nvidia/Qwen3.6-35B-A3B-NVFP4" {
+ t.Fatalf("unexpected served_models: %+v", p0.ServedModels)
+ }
+ if p0.Health != "available" || p0.LoadRatio != 0.25 {
+ t.Fatalf("unexpected health/load_ratio: health=%q load_ratio=%f", p0.Health, p0.LoadRatio)
+ }
+ if len(p0.LifecycleCapabilities) != 2 || p0.LifecycleCapabilities[0] != "list_models" || p0.LifecycleCapabilities[1] != "load_model" {
+ t.Fatalf("unexpected lifecycle_capabilities: %+v", p0.LifecycleCapabilities)
+ }
+
+ // Second provider snapshot (ollama-cpu)
+ p1 := node.ProviderSnapshots[1]
+ if p1.Adapter != "ollama-cpu" || p1.Status != "backlog" || p1.Health != "unavailable" {
+ t.Fatalf("unexpected provider snapshot[1]: %+v", p1)
+ }
+ if p1.Capacity != 2 || p1.InFlight != 2 || p1.Queued != 5 {
+ t.Fatalf("unexpected capacity/in_flight/queued for ollama: capacity=%d in_flight=%d queued=%d", p1.Capacity, p1.InFlight, p1.Queued)
+ }
+
bodyStr := resp.Body.String()
if strings.Contains(bodyStr, "super-secret-token") || strings.Contains(bodyStr, "secret_key") || strings.Contains(bodyStr, "settings") {
t.Fatalf("leaked raw config settings in response: %s", bodyStr)
@@ -536,6 +597,64 @@ func TestEdgeRegistryHTTPHandlersGetLiveStatus(t *testing.T) {
t.Fatalf("config summary missing key %q: %#v", required, rawConfig)
}
}
+
+ // Provider snapshots raw JSON assertions
+ rawProviderSnapshots, ok := rawNode["provider_snapshots"].([]any)
+ if !ok {
+ t.Fatalf("provider_snapshots key missing or not an array: %#v", rawNode)
+ }
+ if len(rawProviderSnapshots) != 2 {
+ t.Fatalf("expected 2 provider_snapshots in raw JSON, got %d", len(rawProviderSnapshots))
+ }
+
+ rawP0, ok := rawProviderSnapshots[0].(map[string]any)
+ if !ok {
+ t.Fatalf("provider_snapshots[0] is not an object: %#v", rawProviderSnapshots[0])
+ }
+ if rawP0["adapter"] != "vllm-gpu" {
+ t.Fatalf("provider_snapshots[0].adapter: got %v", rawP0["adapter"])
+ }
+ if rawP0["capacity"] != float64(4) {
+ t.Fatalf("provider_snapshots[0].capacity: got %v", rawP0["capacity"])
+ }
+ if rawP0["load_ratio"] != float64(0.25) {
+ t.Fatalf("provider_snapshots[0].load_ratio: got %v", rawP0["load_ratio"])
+ }
+ servedModels, ok := rawP0["served_models"].([]any)
+ if !ok || len(servedModels) != 1 {
+ t.Fatalf("provider_snapshots[0].served_models: got %#v", rawP0["served_models"])
+ }
+ if servedModels[0] != "nvidia/Qwen3.6-35B-A3B-NVFP4" {
+ t.Fatalf("provider_snapshots[0].served_models[0]: got %v", servedModels[0])
+ }
+ lifecycleCaps, ok := rawP0["lifecycle_capabilities"].([]any)
+ if !ok || len(lifecycleCaps) != 2 {
+ t.Fatalf("provider_snapshots[0].lifecycle_capabilities: got %#v", rawP0["lifecycle_capabilities"])
+ }
+ if lifecycleCaps[0] != "list_models" || lifecycleCaps[1] != "load_model" {
+ t.Fatalf("provider_snapshots[0].lifecycle_capabilities: got %v", lifecycleCaps)
+ }
+
+ // Second provider snapshot raw check
+ rawP1, ok := rawProviderSnapshots[1].(map[string]any)
+ if !ok {
+ t.Fatalf("provider_snapshots[1] is not an object: %#v", rawProviderSnapshots[1])
+ }
+ if rawP1["adapter"] != "ollama-cpu" || rawP1["health"] != "unavailable" {
+ t.Fatalf("provider_snapshots[1] adapter/health: got %#v", rawP1)
+ }
+ // Zero load_ratio must be present in raw JSON (omitempty 제거 검증)
+ if rawP1["load_ratio"] != float64(0) {
+ t.Fatalf("provider_snapshots[1].load_ratio: expected 0 for idle provider, got %v", rawP1["load_ratio"])
+ }
+
+ // Ensure raw JSON does not leak config internals into provider_snapshots
+ providerSnapshotsStr, _ := json.Marshal(rawP0)
+ for _, forbidden := range []string{"runtime", "settings", "secret"} {
+ if strings.Contains(string(providerSnapshotsStr), forbidden) {
+ t.Fatalf("provider_snapshot leaked raw config key %q: %s", forbidden, providerSnapshotsStr)
+ }
+ }
})
t.Run("MissingEdge", func(t *testing.T) {
diff --git a/apps/edge/internal/edgecmd/edgecmd_test.go b/apps/edge/internal/edgecmd/edgecmd_test.go
index 16c31c2..6b64111 100644
--- a/apps/edge/internal/edgecmd/edgecmd_test.go
+++ b/apps/edge/internal/edgecmd/edgecmd_test.go
@@ -179,6 +179,56 @@ func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
}
}
+func TestNodesListPrintsConfiguredProviders(t *testing.T) {
+ dir := t.TempDir()
+ cfgPath := filepath.Join(dir, "edge.yaml")
+ yaml := `
+server:
+ listen: "0.0.0.0:9090"
+nodes:
+ - id: "node-gpu-01"
+ alias: "gpu-node"
+ token: "token-1"
+ providers:
+ - id: "vllm-gpu"
+ type: "vllm"
+ category: "api"
+ adapter: "vllm-gpu"
+ models:
+ - "nvidia/Qwen3.6-35B-A3B-NVFP4"
+ capacity: 4
+ lifecycle_capabilities:
+ - "list_models"
+ - "load_model"
+`
+ if err := os.WriteFile(cfgPath, []byte(yaml), 0o600); err != nil {
+ t.Fatalf("write %s: %v", cfgPath, err)
+ }
+
+ root := NewRoot(Options{})
+ var out bytes.Buffer
+ root.SetOut(&out)
+ root.SetErr(&out)
+ root.SetArgs([]string{"--config", cfgPath, "nodes", "list"})
+ if err := root.Execute(); err != nil {
+ t.Fatalf("execute nodes list: %v\n%s", err, out.String())
+ }
+
+ got := out.String()
+ for _, want := range []string{
+ "node-gpu-01",
+ "providers: vllm-gpu",
+ "type=vllm/api",
+ "models=nvidia/Qwen3.6-35B-A3B-NVFP4",
+ "capacity=4",
+ "lifecycle=list_models,load_model",
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("nodes list output missing %q:\n%s", want, got)
+ }
+ }
+}
+
func TestPatchYAMLPreservesArtifactBootstrapField(t *testing.T) {
origYAML := `server:
listen: "0.0.0.0:9090"
diff --git a/apps/edge/internal/edgecmd/nodes.go b/apps/edge/internal/edgecmd/nodes.go
index e02472d..fb54f28 100644
--- a/apps/edge/internal/edgecmd/nodes.go
+++ b/apps/edge/internal/edgecmd/nodes.go
@@ -48,8 +48,44 @@ When the server is offline or this command is run standalone, all nodes will sho
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", strings.Repeat("-", 100))
for _, n := range nodes {
fmt.Fprintf(cmd.OutOrStdout(), "%-25s %-20s %-40s %-12s\n", n.ID, n.Alias, n.Token, "offline (configured)")
+ if summary := formatConfiguredProviders(n.Providers); summary != "" {
+ fmt.Fprintf(cmd.OutOrStdout(), " providers: %s\n", summary)
+ }
}
return nil
},
}
}
+
+func formatConfiguredProviders(providers []config.NodeProviderConf) string {
+ if len(providers) == 0 {
+ return ""
+ }
+ summaries := make([]string, 0, len(providers))
+ for _, p := range providers {
+ if p.ID == "" {
+ continue
+ }
+ parts := []string{p.ID}
+ if p.Type != "" || p.Category != "" {
+ typeCategory := string(p.Category)
+ if p.Type != "" && typeCategory != "" {
+ typeCategory = p.Type + "/" + typeCategory
+ } else if p.Type != "" {
+ typeCategory = p.Type
+ }
+ parts = append(parts, "type="+typeCategory)
+ }
+ if len(p.Models) > 0 {
+ parts = append(parts, "models="+strings.Join(p.Models, ","))
+ }
+ if p.Capacity > 0 {
+ parts = append(parts, fmt.Sprintf("capacity=%d", p.Capacity))
+ }
+ if len(p.LifecycleCapabilities) > 0 {
+ parts = append(parts, "lifecycle="+strings.Join(p.LifecycleCapabilities, ","))
+ }
+ summaries = append(summaries, strings.Join(parts, " "))
+ }
+ return strings.Join(summaries, "; ")
+}