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

# Gemini Usage Status Follow-Up Plan

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

**구현 완료 후 `CODE_REVIEW-*-G??.md`의 모든 섹션을 채우는 것이 필수 최종 단계입니다. 이 파일을 채우기 전까지 작업은 완료가 아닙니다.** 아래 체크리스트와 중간/최종 검증을 실제로 실행하고, 구현 내용과 명령 출력을 `CODE_REVIEW-cloud-G07.md`에 기록하세요. `CODE_REVIEW-cloud-G07.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log` 변경, `complete.log` 작성)는 구현 에이전트가 수행하지 않습니다.

## 배경

1차 구현은 Gemini `/stats model` quota parser와 checker를 추가했지만, code review에서 두 Required 이슈가 확인되었다. Gemini checker는 `Gemini CLI` 배너만 보고 command를 보낼 수 있어 실제 TUI readiness가 불안정하다. 또한 계획된 `./bin/edge.sh` + `./bin/node.sh` smoke가 실패했고, 보조 `go run node` smoke로 대체되어 사용자 요청의 bin 기준 검증을 만족하지 못했다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-task/cli_gemini_usage_status/plan_cloud_G07_0.log`
- `agent-task/cli_gemini_usage_status/code_review_cloud_G07_0.log`
- `apps/node/internal/adapters/cli/status/gemini.go`
- `apps/node/internal/adapters/cli/status/gemini_test.go`
- `apps/node/internal/adapters/cli/status/parser.go`
- `apps/node/internal/adapters/cli/status/parser_test.go`
- `apps/node/internal/adapters/cli/status/status.go`
- `apps/node/internal/adapters/cli/status/status_test.go`
- `apps/edge/cmd/edge/console.go`
- `apps/edge/cmd/edge/console_test.go`
- `bin/edge.sh`
- `bin/node.sh`

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

- Gemini readiness: 기존 fake tests는 `Gemini CLI` 배너와 ready prompt를 거의 동시에 출력한다. 배너 후 prompt 전 입력이 발생하면 실패하는 테스트가 필요하다.
- Bin smoke: 기존 fixed smoke가 실패했다. `bin/node.sh`가 임시 node config를 실제 `node serve`에 전달하는지 검증하는 shell smoke가 필요하다.

### 심볼 참조

- renamed/removed symbol 없음.
- 참조 확인 명령: `rg --sort path -n "NewGeminiChecker|GeminiChecker|IOP_NODE_CONFIG|IOP_EDGE_CONFIG|gemini status not implemented|NewGeminiChecker\\(\\)" apps/node/internal/adapters/cli/status bin agent-task/cli_gemini_usage_status`

### 범위 결정 근거

- Gemini `/model`의 모델별 quota는 계속 제외한다. 이번 follow-up은 review Required 이슈만 처리한다.
- proto와 config schema는 수정하지 않는다. Shell wrapper override와 TUI readiness 안정화로 충분하다.
- 기존 `IOP_EDGE_ADDR` 동작은 유지한다. `bin/node.sh`에는 config file override만 추가해 기본 동작을 바꾸지 않는다.

### 빌드 등급

- build lane: `cloud-G07` — TUI/PTY readiness와 bin shell orchestration 검증이 핵심이다.
- review lane: `cloud-G07` — fixed smoke 출력이 실제 재실행과 일치하는지 확인해야 한다.

## 의존 관계 및 구현 순서

1. `REVIEW_API-1`에서 Gemini readiness 조건과 회귀 테스트를 먼저 고친다.
2. `REVIEW_API-2`에서 `bin/node.sh` config override와 bin smoke를 복구한다.

### [REVIEW_API-1] Gemini checker readiness를 prompt-ready 조건으로 제한

#### 문제

`apps/node/internal/adapters/cli/status/gemini.go:97-105`는 `Gemini CLI` 배너만 봐도 ready로 판단하고 300ms 뒤 `/stats model`을 전송한다. 실제 Gemini CLI는 이 배너가 authentication/welcome 단계에서 먼저 보이고 입력 composer는 `? for shortcuts` 또는 prompt/footer 이후 열린다. 이 상태에서는 command가 너무 일찍 보내져 TUI가 무시하거나 다른 상태에 소비할 수 있다.

Before (`apps/node/internal/adapters/cli/status/gemini.go:97`):

```go
// 1. Wait for startup ready prompt.
readyRegex := regexp.MustCompile(`for shortcuts|Gemini CLI|Tips for getting started`)
if _, err := waitForAny([]waitPattern{{name: "ready", pattern: readyRegex}}, 20*time.Second); err != nil {
	return nil, fmt.Errorf("gemini startup failed: %w", err)
}
time.Sleep(300 * time.Millisecond)

// 2. Request /stats model
sendText("\x15/stats model\r")
```

#### 해결 방법

`Gemini CLI` banner-only match를 제거하고 input composer가 열린 뒤에만 매치되는 문구로 제한한다. 우선 후보는 `for shortcuts`, `Type your message`, `workspace (/directory)`다. fake TUI는 배너를 먼저 출력하고 500ms 동안 early stdin을 감시한 뒤 ready prompt를 출력하게 만들어, 기존 구현에서는 실패하고 수정 후 통과하게 한다.

After:

```go
readyRegex := regexp.MustCompile(`for shortcuts|Type your message|workspace \(/directory\)`)
```

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

- [ ] `apps/node/internal/adapters/cli/status/gemini.go` - `readyRegex`에서 `Gemini CLI` 제거
- [ ] `apps/node/internal/adapters/cli/status/gemini.go` - 필요 시 ready wait timeout 에러 tail 개선
- [ ] `apps/node/internal/adapters/cli/status/gemini_test.go` - `TestGeminiCheckerWaitsForInputReady` 추가

#### 테스트 작성

- `apps/node/internal/adapters/cli/status/gemini_test.go` - `TestGeminiCheckerWaitsForInputReady`: fake bash script가 `Gemini CLI v0.42.0` 출력 후 `read -t 0.5 early`로 prompt 전 입력이 오면 exit 4를 낸다. 그 다음 `? for shortcuts`를 출력하고 `/stats model`을 읽어 quota를 반환한다. checker가 early input 없이 정상 quota를 파싱하는지 검증한다.

#### 중간 검증

```bash
go test ./apps/node/internal/adapters/cli/status/... -run 'TestGeminiChecker(WaitsForInputReady|RequestsStatsModel|NoAPICallsReturnsRawUnparsed)' -count=1
```

기대 결과: Gemini checker fake TUI tests가 모두 통과한다. 캐시 출력은 허용하지 않는다.

### [REVIEW_API-2] `bin/node.sh` config override와 bin-only smoke 복구

#### 문제

`bin/node.sh:6`은 항상 `configs/node.yaml`을 사용한다. `bin/node.sh:11`의 `IOP_EDGE_ADDR`은 reachability check에만 쓰이고, `bin/node.sh:36`의 실제 `node serve --config "$CONFIG_FILE"`에는 반영되지 않는다. 그래서 1차 계획의 fixed smoke는 임시 edge `127.0.0.1:19090`에 reachable check만 통과한 뒤 node가 기본 config의 `localhost:9090`으로 연결해 `error: no nodes connected`로 실패했다.

Before (`bin/node.sh:5`):

```bash
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="$REPO_ROOT/configs/node.yaml"
```

#### 해결 방법

`bin/edge.sh`와 같은 패턴으로 `IOP_NODE_CONFIG` override를 추가한다. 최종 smoke는 `/tmp`에 임시 edge config와 node config를 만들고, edge는 `IOP_EDGE_CONFIG`, node는 `IOP_NODE_CONFIG`로 실행한다. 이로써 `./bin/edge.sh`와 `./bin/node.sh`만으로 fake Gemini `/status`를 검증한다.

After:

```bash
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="${IOP_NODE_CONFIG:-$REPO_ROOT/configs/node.yaml}"
```

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

- [ ] `bin/node.sh` - `IOP_NODE_CONFIG` override 추가
- [ ] `agent-task/cli_gemini_usage_status/CODE_REVIEW-cloud-G07.md` - 최종 검증에 fixed bin-only smoke 실제 출력 기록

#### 테스트 작성

별도 unit test는 작성하지 않는다. 변경은 shell wrapper fallback 1줄이며, 최종 bin-only smoke가 동작을 검증한다.

#### 중간 검증

```bash
IOP_NODE_CONFIG=/tmp/non-existent-iop-node.yaml ./bin/node.sh
```

기대 결과: config 파일이 없어서 edge address parsing 단계에서 invalid edge_addr 또는 file read 기반 error로 실패한다. 즉 `bin/node.sh`가 `IOP_NODE_CONFIG` 값을 사용한다. 이 명령은 실패가 기대 결과이므로 실제 stdout/stderr와 exit status를 기록한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/cli/status/gemini.go` | REVIEW_API-1 |
| `apps/node/internal/adapters/cli/status/gemini_test.go` | REVIEW_API-1 |
| `bin/node.sh` | REVIEW_API-2 |
| `agent-task/cli_gemini_usage_status/CODE_REVIEW-cloud-G07.md` | REVIEW_API-2 |

## 최종 검증

```bash
gofmt -l apps/node/internal/adapters/cli/status/gemini.go apps/node/internal/adapters/cli/status/gemini_test.go
```

기대 결과: 출력이 없어야 한다.

```bash
go test ./apps/node/internal/adapters/cli/status/... ./apps/node/internal/adapters/cli/... ./apps/edge/cmd/edge/... -count=1
```

기대 결과: status, cli, edge console tests가 모두 통과한다. 캐시 출력은 허용하지 않는다.

```bash
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
```

기대 결과: 출력 없이 exit 0이어야 한다.

```bash
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"
sed -n '1,120p' "$tmpdir/edge.err"
sed -n '1,160p' "$tmpdir/node.out"
sed -n '1,160p' "$tmpdir/node.err"
rg -n '\[node-local-node-status\] target=gemini session=default|Daily quota: 100% remaining \(resets 24h\)|Used: 0%|Usage limit: 200' "$tmpdir/edge.out"
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
```

기대 결과: `./bin/edge.sh`와 `./bin/node.sh`만으로 edge output에 `[node-local-node-status] target=gemini session=default`, `Daily quota: 100% remaining (resets 24h)`, `Used: 0%`, `Usage limit: 200`이 출력된다. `unexpected command`와 `error: no nodes connected`는 없어야 한다.

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