<!-- task=cli_gemini_usage_status plan=1 tag=REVIEW_API -->

# Code Review Reference - REVIEW_API

## 개요

date=2026-05-16
task=cli_gemini_usage_status, plan=1, tag=REVIEW_API

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

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

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

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] Gemini checker readiness를 prompt-ready 조건으로 제한 | [x] |
| [REVIEW_API-2] `bin/node.sh` config override와 bin-only smoke 복구 | [x] |

## 계획 대비 변경 사항

- 계획에 없던 변경 없음. 계획대로 readyRegex의 `Gemini CLI` 배너를 제거했고 `bin/node.sh`에 `IOP_NODE_CONFIG` override만 추가했다.

## 주요 설계 결정

- Gemini readiness 매칭은 `for shortcuts|Type your message|workspace \(/directory\)`로 한정해 input composer가 열린 뒤에만 ready로 판단한다. 배너만 보고 진행하던 race를 제거하면서 기존 fake test의 `? for shortcuts` 단서는 그대로 유지된다.
- `TestGeminiCheckerWaitsForInputReady`는 banner 이후 0.5s 동안 `read -t 0.5` early stdin 감시로 prompt 전 입력을 감지하면 exit 4를 낸다. 이전 readyRegex(=`Gemini CLI` 매칭)에서는 즉시 입력이 들어가 fail이 발생하고, 수정 후에는 prompt가 출력될 때까지 입력이 지연되어 통과한다.
- `bin/node.sh`는 `bin/edge.sh`와 동일 패턴으로 `IOP_NODE_CONFIG` 환경변수가 있으면 그 경로를 `node serve --config`에 그대로 전달한다. 환경변수가 비면 기존 `configs/node.yaml` 기본값을 유지하므로 사용자 기본 동작에는 영향이 없다.

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

- `Gemini CLI` 배너만으로 ready 처리하지 않고, 입력 composer가 열린 뒤 `/stats model`을 전송하는지 확인한다.
- fake Gemini test가 prompt 전 입력을 감지하면 실패하도록 작성되어 실제 regression을 잡는지 확인한다.
- `bin/node.sh`가 `IOP_NODE_CONFIG`를 실제 `node serve --config`에 전달하는지 확인한다.
- 최종 smoke가 보조 `go run node`가 아니라 `./bin/edge.sh`와 `./bin/node.sh`만으로 통과하는지 확인한다.
- 최종 smoke output에 `error: no nodes connected`와 `unexpected command`가 없는지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

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

### REVIEW_API-1 중간 검증
```
$ go test ./apps/node/internal/adapters/cli/status/... -run 'TestGeminiChecker(WaitsForInputReady|RequestsStatsModel|NoAPICallsReturnsRawUnparsed)' -count=1
ok  	iop/apps/node/internal/adapters/cli/status	7.853s
```

### REVIEW_API-2 중간 검증
```
$ IOP_NODE_CONFIG=/tmp/non-existent-iop-node.yaml ./bin/node.sh
[node] config=/tmp/non-existent-iop-node.yaml
sed: can't read /tmp/non-existent-iop-node.yaml: No such file or directory
EXIT=2
```

`bin/node.sh`가 `IOP_NODE_CONFIG`를 그대로 사용해 `CONFIG_FILE`을 `/tmp/non-existent-iop-node.yaml`로 echo하고, 파일이 없어 sed가 실패하면서 `set -euo pipefail`로 exit 2가 발생했다. override 경로가 실제로 적용된다.

### 최종 검증
```
$ gofmt -l apps/node/internal/adapters/cli/status/gemini.go apps/node/internal/adapters/cli/status/gemini_test.go
GOFMT_OK
```

(gofmt stdout은 비어 있음. `GOFMT_OK`는 명령 뒤에 붙인 sentinel echo.)

```
$ go test ./apps/node/internal/adapters/cli/status/... ./apps/node/internal/adapters/cli/... ./apps/edge/cmd/edge/... -count=1
ok  	iop/apps/node/internal/adapters/cli/status	25.482s
ok  	iop/apps/node/internal/adapters/cli	87.369s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/edge/cmd/edge	0.005s
```

```
$ if rg --sort path -n 'gemini status not implemented|NewGeminiChecker\(\)|readyRegex := regexp.MustCompile\(`.*Gemini CLI' apps/node/internal/adapters/cli/status; then exit 1; fi
RG_OK
```

(rg가 매치를 찾지 못해 exit 1을 반환했고 `if`는 else로 들어가 0으로 종료. sentinel `RG_OK` 출력으로 확인.)

```
$ tmpdir="$(mktemp -d)"
$ mkfifo "$tmpdir/edge.in"
$ cat > "$tmpdir/gemini" <<'SH'
#!/usr/bin/env sh
printf 'Gemini CLI v0.42.0\n'
printf '? for shortcuts\n'
IFS= read -r command
cmd="${command#?}"
if [ "$cmd" != "/stats model" ] && [ "$command" != "/stats model" ]; then
  printf 'unexpected command: %s\n' "$command"
  exit 3
fi
printf 'Auto (Gemini 3) Stats For Nerds\n'
printf '0%% used (Limit resets in 24h)\n'
printf 'Usage limit: 200\n'
printf 'Usage limits span all sessions and reset daily.\n'
IFS= read -r exit_command
exit 0
SH
$ chmod +x "$tmpdir/gemini"
$ perl -0pe 's/listen: "0\.0\.0\.0:9090"/listen: "127.0.0.1:19190"/; s/port: 9092/port: 19192/; s/target: "claude"/target: "gemini"/; s#command: "gemini"#command: "'"$tmpdir"'/gemini"#; s/token: "changeme"/token: "smoketoken"/' configs/edge.yaml > "$tmpdir/edge.yaml"
$ cat > "$tmpdir/node.yaml" <<'YAML'
transport:
  edge_addr: "127.0.0.1:19190"
  token: "smoketoken"
logging:
  level: "info"
  pretty: true
metrics:
  port: 19191
YAML
$ IOP_EDGE_CONFIG="$tmpdir/edge.yaml" ./bin/edge.sh < "$tmpdir/edge.in" > "$tmpdir/edge.out" 2> "$tmpdir/edge.err" &
$ edge_pid=$!
$ exec 3> "$tmpdir/edge.in"
$ IOP_NODE_CONFIG="$tmpdir/node.yaml" ./bin/node.sh > "$tmpdir/node.out" 2> "$tmpdir/node.err" &
$ node_pid=$!
$ sleep 12
$ printf '/nodes\n' >&3 ; sleep 2
$ printf '/status\n' >&3 ; sleep 20
$ printf '/exit\n' >&3
$ wait "$edge_pid"
$ kill "$node_pid" 2>/dev/null || true

$ sed -n '1,220p' "$tmpdir/edge.out"
[edge] config=/tmp/tmp.ZlCbt2yDug/edge.yaml
IOP Edge console listening on 127.0.0.1:19190
Console target node= adapter=cli target=gemini 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>   node-dgx-01 (local-node)
edge> [edge] sent command=status node=local-node adapter=cli target=gemini session=default
[node-local-node-status] target=gemini session=default
Daily quota: 100% remaining (resets 24h)
Used: 0%
Usage limit: 200
edge> bye

$ sed -n '1,120p' "$tmpdir/edge.err"
{"level":"info","ts":1778905857.85,"caller":"transport/server.go:72","msg":"edge listening for nodes","addr":"127.0.0.1:19190"}
{"level":"info","ts":1778905858.33,"caller":"transport/server.go:87","msg":"node connection established"}
{"level":"info","ts":1778905859.54,"caller":"transport/server.go:87","msg":"node connection established"}
{"level":"info","ts":1778905859.54,"caller":"transport/server.go:135","msg":"node registered","node_id":"node-dgx-01","alias":"local-node"}
{"level":"info","ts":1778905891.33,"caller":"transport/server.go:132","msg":"node unregistered","node_id":"node-dgx-01"}

$ sed -n '1,160p' "$tmpdir/node.out"
[node] config=/tmp/tmp.ZlCbt2yDug/node.yaml
[node] waiting for edge at 127.0.0.1:19190 timeout=30s
[node] edge is reachable

$ sed -n '1,160p' "$tmpdir/node.err"
[Fx] PROVIDE	fx.Lifecycle <= go.uber.org/fx.New.func1()
... (fx bootstrap) ...
{"level":"info","msg":"registered with edge","node_id":"node-dgx-01","alias":"local-node"}
{"level":"info","msg":"store ready","dsn":"file:/tmp/iop/workspace/iop.db?cache=shared&mode=rwc"}
[Fx] HOOK OnStart ran successfully in 2.006125ms
[Fx] RUNNING
{"level":"info","msg":"command request","request_id":"status-1778905871328561669","type":"NODE_COMMAND_TYPE_USAGE_STATUS","adapter":"cli","target":"gemini"}
{"level":"info","msg":"disconnected from edge"}

$ rg -n '\[node-local-node-status\] target=gemini session=default|Daily quota: 100% remaining \(resets 24h\)|Used: 0%|Usage limit: 200' "$tmpdir/edge.out"
8:[node-local-node-status] target=gemini session=default
9:Daily quota: 100% remaining (resets 24h)
10:Used: 0%
11:Usage limit: 200

$ if rg -n 'unexpected command|error: no nodes connected' "$tmpdir/edge.out" "$tmpdir/edge.err" "$tmpdir/node.out" "$tmpdir/node.err"; then exit 1; fi
(no match — exit 0)
```

`./bin/edge.sh`와 `./bin/node.sh`만으로 fake Gemini quota 출력이 edge `/status`에 표시되었고, `unexpected command` 또는 `error: no nodes connected`는 어느 출력에도 없다. 임시 fixture는 `/tmp` 아래에만 생성되었고 repo에는 추가 파일이 남지 않았다.

## 코드리뷰 결과

### 종합 판정

PASS

### 차원별 평가

| 차원 | 평가 | 근거 |
|------|------|------|
| correctness | Pass | Gemini checker가 배너가 아니라 input composer ready 신호를 기다린 뒤 `/stats model`을 전송한다. |
| completeness | Pass | 이전 Required 이슈였던 readiness race와 bin-only smoke 실패가 모두 처리되었다. |
| test coverage | Pass | prompt 전 입력을 감지하는 fake Gemini 회귀 테스트와 parser/formatter/bin smoke 검증이 있다. |
| API contract | Pass | 기존 `AgentUsageStatus` 필드와 metadata만 사용하며 proto/config schema 변경 없이 `/status` 계약을 유지한다. |
| code quality | Pass | 변경은 기존 Codex/Claude checker 패턴을 따르고 불필요한 debug print나 dead code가 없다. |
| plan deviation | Pass | follow-up 계획 범위 안에서 수정되었고, 추가 확인한 실제 `gemini --help`에서도 `--skip-trust`/`--screen-reader` 옵션이 존재한다. |
| verification trust | Pass | gofmt, focused/full Go tests, stale-pattern rg, bin-only smoke를 재실행했고 기대 출력과 일치했다. |

### 발견된 문제

없음

### 다음 단계

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