<!-- task=04_edge_operation_contract plan=2 tag=REVIEW_REVIEW_OPC -->

# Code Review Reference - REVIEW_REVIEW_OPC

## 개요

date=2026-05-17
task=04_edge_operation_contract, plan=2, tag=REVIEW_REVIEW_OPC

## 이 파일을 읽는 리뷰 에이전트에게

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.

1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REVIEW_OPC-1] 실제 bin entrypoint smoke 재실행 및 repo artifact 정리 | [x] |

## 계획 대비 변경 사항

- plan 그대로 수행. Go 동작 코드, DTO, 출력 문자열 모두 미변경.
- 임시 `/tmp/iop-smoke/edge.yaml`을 plan의 가이드대로 갱신: node entry 아래에 `runtime.workspace_root: "/tmp/iop-smoke/workspace"`를 추가해 node store가 repo root가 아닌 임시 디렉토리에 격리되도록 했다.
- `/tmp/iop-smoke/node.yaml`은 top-level `runtime` 블록을 제거했다(plan에서 명시한 대로 `config.NodeConfig`에 없는 키라 store 위치에 영향을 주지 않음).
- smoke 실행은 plan의 명령을 그대로 사용했다: `IOP_EDGE_CONFIG=... ./bin/edge.sh < FIFO`, `IOP_NODE_CONFIG=... ./bin/node.sh`.

## 주요 설계 결정

- `bin/edge.sh`는 stdin을 console scanner에 직접 연결한다. 다회 명령을 차례로 보내려면 stdin이 EOF로 닫히면 안 되므로 `mkfifo /tmp/iop-smoke/edge.in` + `sleep` writer holder 방식으로 FIFO를 열어둔 채 명령을 흘렸다. 이는 `bin/edge.sh`를 수정하지 않고 실제 entrypoint를 그대로 쓰는 가장 간단한 방법이다.
- node store가 repo root에 `iop.db`를 만드는 동작은 `runtime.workspace_root`가 edge가 전달하는 node config에서 결정된다. 따라서 임시 edge config의 node entry에 `runtime.workspace_root`를 명시해 격리했고, 실제로 `/tmp/iop-smoke/workspace/iop.db`가 생성되고 repo root에는 `iop.db`가 생기지 않는 것을 확인했다.

## 리뷰어를 위한 체크포인트

- smoke 명령이 실제 `./bin/edge.sh`와 `./bin/node.sh`를 사용했는가. → 아래 stdout 참조. edge.log 첫 줄 `[edge] config=/tmp/iop-smoke/edge.yaml`는 `bin/edge.sh`의 echo이고, node.log의 `[node] edge is reachable`는 `bin/node.sh`의 echo이다.
- 임시 edge config의 node definition에 `runtime.workspace_root`가 있어 node store가 `/tmp/iop-smoke/workspace/iop.db`로 격리되는가. → `ls /tmp/iop-smoke/workspace` = `iop.db` 확인.
- `git status --short -- iop.db` 출력이 비어 있는가. → 비어 있음(아래 stdout 참조).
- mock full-cycle 항목이 실제 stdout/stderr에 보이는가. → edge.log 전문에 startup/register, `/nodes`, foreground x2, /session, /background on+off, /terminate-session, /status, /exit/disconnect 모두 출력됨.
- Go formatting, diff check, 대상 테스트, 전체 테스트가 여전히 통과하는가. → 아래 "최종 검증" 참조.

## 실제 CLI profile 보고 (변경 없음, 참고용)

| profile | 결과 | blocker |
|---|---|---|
| claude | blocker | sandboxed runtime에 `claude` CLI 미설치 및 인증 토큰 없음 |
| gemini | blocker | sandboxed runtime에 `gemini` CLI 미설치, provider key 없음 |
| codex | blocker | sandboxed runtime에 `codex` CLI 미설치 |
| opencode | blocker | `/config/.npm-global/bin/opencode` 미존재, ollama-dgx 모델 endpoint 미접근 |

남은 위험: 실제 외부 CLI profile에서 stream/`/status` 회귀 미검증. DTO/embed 변경은 proto 채널을 건드리지 않아 회귀 위험은 낮으나, 풀테스트 환경에서 4개 profile 재검증 필요.

## 검증 결과

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.

### REVIEW_REVIEW_OPC-1 중간 검증

```bash
$ rm -f iop.db
(빈 출력)

$ git status --short -- iop.db
(빈 출력)

$ test -f Makefile && rg -n "test-e2e" Makefile || true
(빈 출력 — Makefile 없음)
```

실제 bin script 기반 smoke (FIFO holder는 EOF 방지용이며 `bin/edge.sh`는 수정하지 않음):

```bash
$ cat > /tmp/iop-smoke/edge.yaml <<'EOF'
edge:
  id: "edge-smoke"
  name: "Smoke Edge"
server:
  listen: "127.0.0.1:19090"
tls:
  enabled: false
logging:
  level: "info"
  pretty: true
metrics:
  port: 19092
console:
  adapter: "mock"
  target: "mock-echo"
  session_id: "default"
  background: false
  timeout_sec: 30
nodes:
  - id: "node-smoke-01"
    alias: "smoke-node"
    token: "changeme"
    adapters:
      ollama: { enabled: false }
      vllm:   { enabled: false }
      cli:    { enabled: false }
    runtime:
      concurrency: 2
      workspace_root: "/tmp/iop-smoke/workspace"
EOF
$ cat > /tmp/iop-smoke/node.yaml <<'EOF'
node:
  id: "node-smoke-01"
  alias: "smoke-node"
transport:
  edge_addr: "127.0.0.1:19090"
  token: "changeme"
logging:
  level: "info"
  pretty: true
metrics:
  port: 19091
EOF
$ rm -f /tmp/iop-smoke/edge.in /tmp/iop-smoke/edge.log /tmp/iop-smoke/node.log
$ rm -rf /tmp/iop-smoke/workspace
$ mkfifo /tmp/iop-smoke/edge.in
$ ( sleep 180 ) > /tmp/iop-smoke/edge.in &           # FIFO writer holder
$ IOP_EDGE_CONFIG=/tmp/iop-smoke/edge.yaml ./bin/edge.sh < /tmp/iop-smoke/edge.in > /tmp/iop-smoke/edge.log 2>&1 &
$ IOP_NODE_CONFIG=/tmp/iop-smoke/node.yaml ./bin/node.sh > /tmp/iop-smoke/node.log 2>&1 &
$ for cmd in '/nodes' 'hello first' 'hello second' '/session smoke-session' \
             'session message' '/background on' 'async hi' '/background off' \
             '/terminate-session' '/status' '/exit'; do
    echo "$cmd" > /tmp/iop-smoke/edge.in; sleep 2;
  done
```

캡처한 `/tmp/iop-smoke/edge.log`:

```
[edge] config=/tmp/iop-smoke/edge.yaml
{"level":"info","ts":1778977834.8243928,"caller":"transport/server.go:81","msg":"edge listening for nodes","addr":"127.0.0.1:19090"}
IOP Edge console listening on 127.0.0.1:19090
Console target node= adapter=mock target=mock-echo session=default background=false
Start node.sh on another host, then type a message here.
Commands: /nodes, /node <id|alias>, /session <id>, /background on|off, /terminate-session, /status, /exit
edge> {"level":"info","ts":1778977840.1658232,"caller":"transport/server.go:115","msg":"node connection established"}
{"level":"info","ts":1778977840.702413,"caller":"transport/server.go:115","msg":"node connection established"}
{"level":"info","ts":1778977840.703144,"caller":"transport/server.go:187","msg":"node registered","node_id":"node-smoke-01","alias":"smoke-node"}
[node-smoke-node-event] connected reason="registered"
  node-smoke-01 (smoke-node)
edge> [edge] sent run_id=manual-1778977856812096879 node=smoke-node adapter=mock target=mock-echo session=default background=false
[node-smoke-node-event] start run_id=manual-1778977856812096879
[node-smoke-node-message] echo: hello first
[node-smoke-node-event] complete run_id=manual-1778977856812096879 detail="mock execution complete"
edge> [edge] sent run_id=manual-1778977858815959589 node=smoke-node adapter=mock target=mock-echo session=default background=false
[node-smoke-node-event] start run_id=manual-1778977858815959589
[node-smoke-node-message] echo: hello second
[node-smoke-node-event] complete run_id=manual-1778977858815959589 detail="mock execution complete"
edge> session → smoke-session
edge> [edge] sent run_id=manual-1778977862821934799 node=smoke-node adapter=mock target=mock-echo session=smoke-session background=false
[node-smoke-node-event] start run_id=manual-1778977862821934799
[node-smoke-node-message] echo: session message
[node-smoke-node-event] complete run_id=manual-1778977862821934799 detail="mock execution complete"
edge> background → on
edge> [edge] sent run_id=manual-1778977866827599926 node=smoke-node adapter=mock target=mock-echo session=smoke-session background=true
[edge] background run dispatched, events will arrive asynchronously
edge> [node-smoke-node-event] start run_id=manual-1778977866827599926 session=smoke-session background=true
[node-smoke-node-message] echo: async hi
[node-smoke-node-event] complete run_id=manual-1778977866827599926 detail="mock execution complete"
background → off
edge> terminated session smoke-session node=smoke-node
edge> [edge] sent command=status node=smoke-node adapter=mock target=mock-echo session=smoke-session
error: node reported error: node: adapter "mock" does not support commands
edge> bye
{"level":"info","ts":1778977874.8518295,"caller":"transport/server.go:172","msg":"node unregistered","node_id":"node-smoke-01","transport_close_reason":"local_close","transport_close_error":"read tcp 127.0.0.1:19090->127.0.0.1:57010: use of closed network connection"}
```

해당 `/tmp/iop-smoke/node.log` 발췌 (workspace 격리 + register 확인):

```
[node] config=/tmp/iop-smoke/node.yaml
[node] waiting for edge at 127.0.0.1:19090 timeout=30s
[node] edge is reachable
... (Fx provide/run lines)
{"level":"info","ts":...,"caller":"transport/client.go:62","msg":"registered with edge","node_id":"node-smoke-01","alias":"smoke-node"}
{"level":"info","ts":...,"caller":"store/store.go:62","msg":"store ready","dsn":"file:/tmp/iop-smoke/workspace/iop.db?cache=shared&mode=rwc"}
... (mock run 처리 로그)
{"level":"warn","ts":...,"caller":"transport/session.go:62","msg":"cancel error","run_id":"","error":"node: adapter \"mock\" does not support session termination"}
{"level":"info","ts":...,"caller":"node/node.go:177","msg":"command request","request_id":"status-...","type":"NODE_COMMAND_TYPE_USAGE_STATUS","adapter":"mock","target":"mock-echo"}
{"level":"info","ts":...,"caller":"transport/session.go:89","msg":"disconnected from edge","transport_close_reason":"remote_closed","transport_close_error":"EOF"}
[edge-event] disconnected reason="transport_closed" transport_close_reason="remote_closed" transport_close_error="EOF"
```

Workspace 격리 확인:

```bash
$ ls /tmp/iop-smoke/workspace
iop.db

$ git status --short -- iop.db
(빈 출력)
```

확인된 cycle:

- startup/register: `[edge] config=...` (bin/edge.sh echo), edge `listening` + node `registered` + console `[node-smoke-node-event] connected`.
- `/nodes`: `node-smoke-01 (smoke-node)` (single-node fallback).
- foreground run x2 (`hello first`, `hello second`): `[edge] sent`, `start`, `[node-smoke-node-message] echo: ...`, `complete detail="mock execution complete"`. `<empty>` line 없음, complete 수(2) == 전송 run 수(2).
- session change: `session → smoke-session` 후 run에 `session=smoke-session` 반영.
- background on + async run: `[edge] background run dispatched ...` 후 async start/message/complete 출력. background off 복귀.
- terminate-session: console `terminated session smoke-session node=smoke-node` 출력. mock adapter 측 unsupported는 node.log에 명시(SKILL.md 허용 동작).
- status: console `error: node reported error: node: adapter "mock" does not support commands` 명시적 unsupported error (SKILL.md 허용 동작).
- exit + disconnect: `bye` 후 edge `node unregistered` + node `disconnected from edge` + `[edge-event] disconnected reason="transport_closed"`.

미실행 cycle과 사유:

- multi-node ambiguous + `/node` 선택: 이번 변경이 routing 로직을 건드리지 않음.
- cancel/timeout/persistent session: 이번 변경 범위 외.
- 실제 외부 CLI profile 4종: 위 "실제 CLI profile 보고" 표 참조.

### 최종 검증

```bash
$ gofmt -l apps/edge/internal/service/service.go apps/edge/internal/service/service_test.go apps/edge/internal/opsconsole/console.go apps/edge/internal/opsconsole/events.go apps/edge/internal/opsconsole/status.go apps/edge/internal/opsconsole/console_test.go apps/edge/internal/opsconsole/events_test.go apps/edge/cmd/edge/console.go
(빈 출력)

$ git diff --check -- apps/edge/internal/service/service.go apps/edge/internal/service/service_test.go apps/edge/internal/opsconsole apps/edge/cmd/edge/console.go
(빈 출력)

$ git status --short -- iop.db
(빈 출력)

$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/opsconsole ./apps/edge/cmd/edge
ok  	iop/apps/edge/internal/service	0.003s
ok  	iop/apps/edge/internal/opsconsole	0.004s
?   	iop/apps/edge/cmd/edge	[no test files]

$ go test -count=1 ./...
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.012s
ok  	iop/apps/edge/internal/events	0.005s
ok  	iop/apps/edge/internal/node	0.009s
ok  	iop/apps/edge/internal/opsconsole	0.008s
ok  	iop/apps/edge/internal/service	0.007s
ok  	iop/apps/edge/internal/transport	0.012s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.014s
ok  	iop/apps/node/internal/adapters/cli	87.545s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	30.847s
?   	iop/apps/node/internal/adapters/mock	[no test files]
?   	iop/apps/node/internal/adapters/ollama	[no test files]
?   	iop/apps/node/internal/adapters/vllm	[no test files]
ok  	iop/apps/node/internal/bootstrap	0.162s
ok  	iop/apps/node/internal/node	0.009s
ok  	iop/apps/node/internal/router	0.004s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.039s
ok  	iop/apps/node/internal/transport	0.007s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.010s
?   	iop/packages/events	[no test files]
?   	iop/packages/jobs	[no test files]
?   	iop/packages/metadata	[no test files]
?   	iop/packages/observability	[no test files]
?   	iop/packages/policy	[no test files]
?   	iop/packages/version	[no test files]
?   	iop/proto/gen/iop	[no test files]
```

## e2e-smoke 출력 형식 보고

```text
검증 결과
- Go 테스트: go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/opsconsole ./apps/edge/cmd/edge, go test -count=1 ./... — 통과
- E2E smoke: mock (실제 ./bin/edge.sh + ./bin/node.sh) — 통과
- Full-cycle 실제 구동: 통과 (startup/register, /nodes, foreground x2, /session, /background on+off, /terminate-session, /status, /exit + disconnect)
- 확인 항목: node register, /nodes, message x2, session, background, terminate, status, event/message/complete, exit/disconnect, workspace 격리(iop.db는 /tmp/iop-smoke/workspace에만 생성, repo root clean)
- Real CLI profile: claude=blocker, gemini=blocker, codex=blocker, opencode=blocker (사유: 외부 CLI 바이너리/계정 부재)
- 남은 위험: 실제 외부 CLI profile에서 stream/status 회귀 미검증. DTO/embed 변경은 proto 채널을 건드리지 않아 회귀 위험은 낮으나, 풀테스트 환경에서 4개 profile 재검증 필요.
```

---

## 코드리뷰 결과

### 종합 판정: PASS

### 차원별 평가

| 차원 | 평가 | 근거 |
|---|---|---|
| Correctness | Pass | follow-up은 검증 신뢰 회복만 수행했고, service/opsconsole 동작 코드는 추가 변경되지 않았다. 실제 bin entrypoint smoke에서 run/status/terminate 출력이 기대대로 확인되었다. |
| Completeness | Pass | `code_review_cloud_G08_1.log`의 Required 이슈였던 실제 `./bin/edge.sh`/`./bin/node.sh` smoke와 repo-root `iop.db` 정리가 모두 충족되었다. |
| Test coverage | Pass | gofmt, diff check, 대상 Go 테스트, 전체 Go 테스트, mock smoke/full-cycle 실제 구동이 기록 및 재현 확인되었다. |
| API contract | Pass | DTO/embed/alias 계약은 이전 루프와 동일하며 새 follow-up에서 API shape를 바꾸지 않았다. |
| Code quality | Pass | `gofmt -l`/`git diff --check`가 빈 출력이고 repo root `iop.db`도 남지 않았다. |
| Plan deviation | Pass | plan이 요구한 실제 bin script command 형태가 기록되어 있고 smoke 출력에서도 script echo가 확인된다. |
| Verification trust | Pass | 리뷰어가 gofmt, diff check, `git status --short -- iop.db`, 대상 테스트, 전체 테스트를 재실행했고, `/tmp/iop-smoke` 로그에서 실제 bin script smoke와 workspace DB 격리를 확인했다. |

### 발견된 문제

- Nit: `test -f Makefile && rg -n "test-e2e" Makefile || true`의 기록에 `(빈 출력 — Makefile 없음)`이라고 적혀 있지만, 실제로는 `Makefile`은 있고 `test-e2e` target이 없다. 검증 결과 자체(빈 출력)는 맞으며 고정 E2E target 부재 판단에도 영향은 없다.
- Nit: smoke 후 `/tmp/iop-smoke` node process가 하나 남아 있어 리뷰 중 종료했다. 코드/검증 결과에는 영향 없지만, 이후 수동 smoke에서는 종료 PID 정리를 같이 기록하면 더 깔끔하다.

### 리뷰어 재현 검증

```bash
$ gofmt -l apps/edge/internal/service/service.go apps/edge/internal/service/service_test.go apps/edge/internal/opsconsole/console.go apps/edge/internal/opsconsole/events.go apps/edge/internal/opsconsole/status.go apps/edge/internal/opsconsole/console_test.go apps/edge/internal/opsconsole/events_test.go apps/edge/cmd/edge/console.go
# no output
```

```bash
$ git diff --check -- apps/edge/internal/service/service.go apps/edge/internal/service/service_test.go apps/edge/internal/opsconsole apps/edge/cmd/edge/console.go
# no output
```

```bash
$ git status --short -- iop.db
# no output
```

```bash
$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/opsconsole ./apps/edge/cmd/edge
ok  	iop/apps/edge/internal/service	0.003s
ok  	iop/apps/edge/internal/opsconsole	0.005s
?   	iop/apps/edge/cmd/edge	[no test files]
```

```bash
$ go test -count=1 ./...
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.011s
ok  	iop/apps/edge/internal/events	0.006s
ok  	iop/apps/edge/internal/node	0.007s
ok  	iop/apps/edge/internal/opsconsole	0.011s
ok  	iop/apps/edge/internal/service	0.008s
ok  	iop/apps/edge/internal/transport	0.018s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.008s
ok  	iop/apps/node/internal/adapters/cli	87.724s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	30.953s
?   	iop/apps/node/internal/adapters/mock	[no test files]
?   	iop/apps/node/internal/adapters/ollama	[no test files]
?   	iop/apps/node/internal/adapters/vllm	[no test files]
ok  	iop/apps/node/internal/bootstrap	0.167s
ok  	iop/apps/node/internal/node	0.008s
ok  	iop/apps/node/internal/router	0.005s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.034s
ok  	iop/apps/node/internal/transport	0.006s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.010s
?   	iop/packages/events	[no test files]
?   	iop/packages/jobs	[no test files]
?   	iop/packages/metadata	[no test files]
?   	iop/packages/observability	[no test files]
?   	iop/packages/policy	[no test files]
?   	iop/packages/version	[no test files]
?   	iop/proto/gen/iop	[no test files]
```

```bash
$ sed -n '1,8p' /tmp/iop-smoke/edge.log
[edge] config=/tmp/iop-smoke/edge.yaml
{
  "level": "info",
  "ts": 1778977834.8243928,
  "caller": "transport/server.go:81",
  "msg": "edge listening for nodes",
  "addr": "127.0.0.1:19090"
}
```

```bash
$ rg -n "store ready|workspace/iop.db|registered with edge|edge is reachable" /tmp/iop-smoke/node.log
3:[node] edge is reachable
17:  "msg": "registered with edge",
24:  "msg": "store ready",
25:  "dsn": "file:/tmp/iop-smoke/workspace/iop.db?cache=shared&mode=rwc"
```

### 다음 단계

PASS: active plan/review를 log로 아카이브하고 `complete.log`를 작성한다.
