# Gemini Usage Status Plan ## 이 파일을 읽는 구현 에이전트에게 **구현 완료 후 `CODE_REVIEW-*-G??.md`의 모든 섹션을 채우는 것이 필수 최종 단계입니다. 이 파일을 채우기 전까지 작업은 완료가 아닙니다.** 아래 체크리스트와 중간/최종 검증을 실제로 실행하고, 구현 내용과 명령 출력을 `CODE_REVIEW-cloud-G07.md`에 기록하세요. `CODE_REVIEW-cloud-G07.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log` 변경, `complete.log` 작성)는 구현 에이전트가 수행하지 않습니다. ## 배경 `/status` console 명령은 Codex와 Claude 사용량을 TUI에서 조회해 edge console에 표시하지만, Gemini는 아직 `gemini status not implemented`를 반환한다. 실제 Gemini CLI `0.42.0` probe와 설치된 CLI 소스 확인 결과, `/stats model`에 표시되는 전체 daily quota 블록만 이번 요구 범위에 해당한다. Gemini의 `/model`에 나오는 모델별 quota와 세션별 model stats 표는 이번 구현에서 제외한다. ## 분석 결과 ### 읽은 파일 - `agent-ops/rules/project/rules.md` - `agent-ops/rules/project/domain/node/rules.md` - `agent-ops/skills/common/plan/SKILL.md` - `agent-task/cli_usage_status_command/plan_0.log` - `agent-task/cli_claude_usage_status/plan_local_G06_0.log` - `bin/edge.sh` - `bin/node.sh` - `configs/edge.yaml` - `configs/node.yaml` - `apps/node/internal/adapters/cli/cli.go` - `apps/node/internal/adapters/cli/status/gemini.go` - `apps/node/internal/adapters/cli/status/status.go` - `apps/node/internal/adapters/cli/status/parser.go` - `apps/node/internal/adapters/cli/status/status_test.go` - `apps/node/internal/adapters/cli/status/parser_test.go` - `apps/node/internal/adapters/cli/status/codex_test.go` - `apps/node/internal/adapters/cli/status/claude_test.go` - `apps/edge/cmd/edge/console.go` - `apps/edge/cmd/edge/console_test.go` - `/config/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-CHERUG6W.js` - `/config/.npm-global/lib/node_modules/@google/gemini-cli/bundle/interactiveCli-AG2YWL2O.js` ### 테스트 커버리지 공백 - Gemini checker 선택: 기존 `status_test.go`는 Codex/Claude command 보존만 검증한다. Gemini도 profile command를 보존하고 빈 command는 `"gemini"`로 보정하는 테스트가 필요하다. - Gemini `/stats model` quota 파싱: 기존 `parser_test.go`는 Codex와 Claude 출력만 검증한다. `0% used (Limit resets in 24h)`와 `Usage limit: 200`을 전체 daily quota로 파싱하는 테스트가 필요하다. - Gemini PTY 조작: `gemini.go`는 stub이라 fake TUI로 `/stats model` 입력, quota 출력 파싱, 종료 입력 흐름을 검증하는 테스트가 없다. - Edge `/status` 표시: `formatUsageStatus`는 `DailyLimit`/`WeeklyLimit`만 표시한다. Gemini metadata의 `used_percent`와 `usage_limit`이 있을 때 `Used`와 `Usage limit`이 출력되는 테스트가 필요하다. - Bin smoke: 기존 테스트는 package 단위다. `./bin/edge.sh`와 `./bin/node.sh`를 통해 `target=gemini`의 `/status`가 node command 경로로 연결되는지 deterministic fake Gemini로 검증해야 한다. ### 심볼 참조 - renamed/removed symbol 없음. - 참조 확인 명령: `rg --sort path -n "NewGeminiChecker|GeminiChecker|ParseStatusOutput|formatUsageStatus|CheckUsage|NewChecker|DailyLimit|WeeklyLimit|daily_label|weekly_label|UsageStatus|AgentUsageStatus" apps/node/internal/adapters/cli apps/edge/cmd/edge proto/iop proto/gen/iop packages configs` - `NewGeminiChecker` 호출부는 현재 `apps/node/internal/adapters/cli/status/status.go:61` 1곳이다. 시그니처 변경 시 `status.go`, `status_test.go`, 새 `gemini_test.go`를 함께 갱신한다. - `formatUsageStatus` 호출부는 `apps/edge/cmd/edge/console.go:352` 1곳이고 테스트는 `apps/edge/cmd/edge/console_test.go:170`, `:187`, `:208` 주변이다. ### 범위 결정 근거 - `proto/iop/runtime.proto`와 `proto/gen/iop/*.pb.go`는 수정하지 않는다. Gemini의 `used_percent`와 `usage_limit`은 기존 `AgentUsageStatus.metadata`로 전달한다. - Gemini `/model`의 모델별 quota는 구현하지 않는다. 사용자가 이번 후속에서 전체 quota만 필요하다고 범위를 좁혔고, `/status`의 기존 구조도 daily/weekly summary 중심이다. - Gemini status 조회는 실행 profile args(`--approval-mode`, `-p`, `--output-format`)를 재사용하지 않는다. Claude/Codex status와 같이 profile command path만 재사용하는 별도 TUI 조회로 둔다. - `configs/edge.yaml`의 기본 `console.target`은 변경하지 않는다. Bin smoke는 `bin/edge.sh`의 config override와 `/tmp` 임시 config로 `target=gemini`를 검증한다. - 실제 Gemini CLI `0.42.0`은 `/stats model` 세션에 API 호출이 없으면 `No API calls have been made in this session.`만 렌더한다. 구현은 이를 parsed quota로 가장하지 않고 raw fallback 또는 명확한 미파싱 상태로 드러낸다. ### 빌드 등급 - build lane: `cloud-G07` — Gemini TUI/PTY 자동화, stdout repaint/스크린리더 출력 파싱, bin script orchestration이 성공 조건에 포함된다. - review lane: `cloud-G07` — fake TUI unit test와 bin smoke 출력 신뢰성, `/stats model` no-quota 조건 처리를 실제 구현과 대조해야 한다. ## 의존 관계 및 구현 순서 1. `API-1`에서 Gemini quota parser를 먼저 추가한다. 2. `API-2`에서 GeminiChecker가 `/stats model`을 실행하고 parser를 호출하게 한다. 3. `API-3`에서 edge console이 Gemini quota metadata를 출력하게 한다. 4. `API-4`에서 bin smoke가 임시 Gemini config로 재현 가능하도록 `bin/edge.sh` config override를 추가한다. ### [API-1] Gemini `/stats model` 전체 quota parser 추가 #### 문제 `apps/node/internal/adapters/cli/status/parser.go:10-24`는 Codex `5h limit`/`Weekly limit`와 Claude `Current session`/`Current week`만 인식한다. Gemini `/stats model`의 전체 quota는 `0% used (Limit resets in 24h)`와 `Usage limit: 200` 형태라 현재 parser는 `DailyLimit`과 reset을 채우지 못하고 raw fallback만 남긴다. 이번 요구는 모델별 usage가 아니라 전체 daily quota이므로 Gemini output은 `DailyLimit`에 remaining percent, `DailyResetTime`에 reset interval, metadata에 used percent와 numeric limit을 넣는다. Before (`apps/node/internal/adapters/cli/status/parser.go:10`): ```go var ( dailyLimitRegex = regexp.MustCompile(`5h limit:.*?\]\s*([^%\n]+%)[^\(]*\(resets\s+([^\)]+)\)`) weeklyLimitRegex = regexp.MustCompile(`Weekly limit:.*?\]\s*([^%\n]+%)[^\(]*\(resets\s+([^\)]+)\)`) // Section header locators — they only locate the start of each block; they // do NOT span forward into other blocks. This prevents the previous lazy // `.*?` regex from accidentally pairing a session header with the week's // "X% used / Resets Y" payload when cursor positioning in the raw PTY // stream interleaves the two sections out of order. claudeSessionHeaderRegex = regexp.MustCompile(`(?i)Current\s+session\b`) claudeWeekHeaderRegex = regexp.MustCompile(`(?i)Current\s+week\b(?:\s*\(all\s+models\))?`) // Payload extractor used *within* a single section slice. claudePayloadRegex = regexp.MustCompile(`(?is)(\d+(?:\.\d+)?)%\s+used.*?Resets\s+([^\n]+)`) ) ``` Before (`apps/node/internal/adapters/cli/status/parser.go:34`): ```go func ParseStatusOutput(raw string) (*UsageStatus, error) { visible := renderVisibleScreen(raw, 60, 200) cleanRaw := cleanANSI(raw) // Prefer whichever input yields a more complete Claude parse: a full // session+week match beats a partial one, which beats nothing. visStatus := &UsageStatus{RawOutput: visible} rawStatus := &UsageStatus{RawOutput: cleanRaw} visStatus.parseClaudeUsage(visible) rawStatus.parseClaudeUsage(cleanRaw) if pick := pickMoreComplete(visStatus, rawStatus); pick != nil { return pick, nil } // Neither input produced Claude data — try Codex. rawStatus.parseCodexUsage(cleanRaw) return rawStatus, nil } ``` #### 해결 방법 Gemini 전용 parser를 추가한다. `0% used (Limit resets in 24h)`는 `used_percent=0%`, `DailyLimit=100%`, `DailyResetTime=24h`로 변환한다. `Usage limit: 200`은 metadata `usage_limit=200`으로 보존한다. `Limit reached, resets in 5h`도 boundary로 처리해 `DailyLimit=0%`, `used_percent=100%`, `DailyResetTime=5h`가 되게 한다. `No API calls have been made in this session.`은 파싱 실패가 맞으므로 raw output만 유지한다. After: ```go var ( geminiUsedQuotaRegex = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)%\s+used\s+\(Limit resets in ([^)]+)\)`) geminiLimitReachedRegex = regexp.MustCompile(`(?i)Limit reached,\s*resets in ([^\n]+)`) geminiUsageLimitRegex = regexp.MustCompile(`(?i)Usage limit:\s*([0-9][0-9,]*)`) ) func (s *UsageStatus) parseGeminiQuota(cleanRaw string) bool { // Fill DailyLimit as remaining percent. Store Gemini-only fields in Metadata. } ``` `ParseStatusOutput` 순서는 Claude visible/raw 선택 뒤 Gemini quota, 그 다음 Codex로 둔다. Gemini는 daily 전체 quota만 채우고 `WeeklyLimit`은 비워 둔다. #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/status/parser.go` - Gemini quota regex 추가 - [ ] `apps/node/internal/adapters/cli/status/parser.go` - `parseGeminiQuota` 추가, `DailyLimit`, `DailyResetTime`, metadata 설정 - [ ] `apps/node/internal/adapters/cli/status/parser.go` - `ParseStatusOutput`에서 Claude 실패 후 Gemini, Codex 순서로 파싱 - [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - `TestParseStatusOutput_GeminiStatsQuota` 추가 - [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - `TestParseStatusOutput_GeminiLimitReached` 추가 - [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - `TestParseStatusOutput_GeminiNoAPICallsLeavesRawOnly` 추가 #### 테스트 작성 - `apps/node/internal/adapters/cli/status/parser_test.go` - `TestParseStatusOutput_GeminiStatsQuota`: sample에 `0% used (Limit resets in 24h)`, `Usage limit: 200`을 넣고 `DailyLimit == "100%"`, `DailyResetTime == "24h"`, metadata `daily_label == "Daily quota"`, `used_percent == "0%"`, `usage_limit == "200"`을 검증한다. - `apps/node/internal/adapters/cli/status/parser_test.go` - `TestParseStatusOutput_GeminiLimitReached`: `Limit reached, resets in 5h`, `Usage limit: 200`에서 `DailyLimit == "0%"`, metadata `used_percent == "100%"`를 검증한다. - `apps/node/internal/adapters/cli/status/parser_test.go` - `TestParseStatusOutput_GeminiNoAPICallsLeavesRawOnly`: `No API calls have been made in this session.`에서 parsed limit이 비어 있고 raw output은 남는지 검증한다. #### 중간 검증 ```bash go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput_(Gemini|Codex|Claude)' -count=1 ``` 기대 결과: 새 Gemini parser tests와 기존 Codex/Claude parser tests가 모두 통과한다. 캐시 출력은 허용하지 않으므로 `-count=1`을 유지한다. ### [API-2] GeminiChecker를 PTY 기반 `/stats model` 조회로 구현 #### 문제 `apps/node/internal/adapters/cli/status/gemini.go:8-16`은 stub이라 `/status`가 `cli/gemini` target에 대해 항상 `gemini status not implemented`를 반환한다. `apps/node/internal/adapters/cli/status/status.go:60-61`도 `NewGeminiChecker()`에 profile command를 전달하지 않아 config의 command path를 보존하지 않는다. Before (`apps/node/internal/adapters/cli/status/gemini.go:1`): ```go package status import ( "context" "fmt" ) type GeminiChecker struct{} func NewGeminiChecker() *GeminiChecker { return &GeminiChecker{} } func (g *GeminiChecker) Check(ctx context.Context) (*UsageStatus, error) { return nil, fmt.Errorf("gemini status not implemented") } ``` Before (`apps/node/internal/adapters/cli/status/status.go:60`): ```go case agent == "gemini" || cmdBase == "gemini": return NewGeminiChecker(), nil ``` #### 해결 방법 Codex/Claude checker 구조를 따라 `GeminiChecker`에 `command string`을 보관하고, 빈 command는 `"gemini"`로 보정한다. `Check`는 `exec.CommandContext(ctx, g.command, "--skip-trust", "--screen-reader")`를 PTY로 띄운 뒤 ready prompt를 기다리고, `Ctrl+U`, `/stats model`, Enter를 전송한다. `Usage limit:`, `Limit reached`, 또는 `No API calls have been made in this session.` 중 하나를 기다린 뒤 flush하고 `ParseStatusOutput`을 호출한다. 종료는 `Esc`, `Ctrl+U`, `/quit\r` 순서로 시도하고 defer kill을 유지한다. After: ```go type GeminiChecker struct { command string } func NewGeminiChecker(command string) *GeminiChecker { if command == "" { command = "gemini" } return &GeminiChecker{command: command} } ``` `--screen-reader`는 실제 probe에서 `/model` full name과 `/stats model` 텍스트를 파싱하기 쉬운 형태로 출력했다. 이 flag는 status 조회 전용이며 profile args와 섞지 않는다. #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/status/gemini.go` - imports 추가: `context`, `fmt`, `io`, `os`, `os/exec`, `regexp`, `strings`, `time`, `github.com/creack/pty` - [ ] `apps/node/internal/adapters/cli/status/gemini.go` - command field와 constructor 보정 추가 - [ ] `apps/node/internal/adapters/cli/status/gemini.go` - PTY startup, chunks reader, `waitForAny`, `sendText`, `/stats model` 입력, output flush, graceful quit 구현 - [ ] `apps/node/internal/adapters/cli/status/status.go` - `NewGeminiChecker(profile.Command)` 호출로 변경 - [ ] `apps/node/internal/adapters/cli/status/status_test.go` - `TestNewChecker_GeminiUsesProfileCommand` 추가 - [ ] `apps/node/internal/adapters/cli/status/status_test.go` - `TestNewChecker_GeminiEmptyCommandDefaultsToGemini` 추가 - [ ] `apps/node/internal/adapters/cli/status/gemini_test.go` - fake TUI 기반 checker tests 추가 #### 테스트 작성 - `apps/node/internal/adapters/cli/status/status_test.go` - `TestNewChecker_GeminiUsesProfileCommand`: `NewChecker("gemini", config.CLIProfileConf{Command: "/tmp/mygemini"})`가 `*GeminiChecker`를 반환하고 `command == "/tmp/mygemini"`인지 검증한다. - `apps/node/internal/adapters/cli/status/status_test.go` - `TestNewChecker_GeminiEmptyCommandDefaultsToGemini`: 빈 command에서 `command == "gemini"`인지 검증한다. - `apps/node/internal/adapters/cli/status/gemini_test.go` - `TestGeminiCheckerRequestsStatsModel`: fake `gemini`가 `/stats model`을 받았는지 확인하고 quota block을 출력하면 checker가 `DailyLimit == "100%"`, `DailyResetTime == "24h"`, metadata `usage_limit == "200"`을 반환하는지 검증한다. - `apps/node/internal/adapters/cli/status/gemini_test.go` - `TestGeminiCheckerNoAPICallsReturnsRawUnparsed`: fake가 `No API calls have been made in this session.`을 출력하면 error 없이 raw fallback status가 반환되고 parsed limit은 비는지 검증한다. #### 중간 검증 ```bash go test ./apps/node/internal/adapters/cli/status/... -run 'Test(NewChecker_Gemini|GeminiChecker)' -count=1 ``` 기대 결과: Gemini checker factory와 fake TUI tests가 통과한다. 외부 real `gemini` binary에는 의존하지 않는다. ### [API-3] Edge `/status`에 Gemini quota metadata 출력 추가 #### 문제 `apps/edge/cmd/edge/console.go:364-374`는 `DailyLimit`과 `WeeklyLimit`만 출력한다. Gemini 전체 quota의 `Usage limit: 200`과 `0% used`는 `AgentUsageStatus.metadata`로 전달해야 protobuf 변경 없이 유지할 수 있지만, 현재 formatter는 metadata를 label 외에는 보여주지 않는다. Before (`apps/edge/cmd/edge/console.go:364`): ```go hasParsedLimits := false dailyLabel := usageStatusLabel(status.GetMetadata(), "daily_label", "Daily limit") if status.GetDailyLimit() != "" { fmt.Fprintf(out, "%s: %s remaining%s\n", dailyLabel, status.GetDailyLimit(), resetSuffix(status.GetDailyResetTime())) hasParsedLimits = true } weeklyLabel := usageStatusLabel(status.GetMetadata(), "weekly_label", "Weekly limit") if status.GetWeeklyLimit() != "" { fmt.Fprintf(out, "%s: %s remaining%s\n", weeklyLabel, status.GetWeeklyLimit(), resetSuffix(status.GetWeeklyResetTime())) hasParsedLimits = true } ``` #### 해결 방법 `formatUsageStatus`에 metadata 출력 helper를 작게 추가한다. `used_percent`가 있으면 `Used: 0%`, `usage_limit`이 있으면 `Usage limit: 200`을 parsed limits 아래에 출력한다. 이 metadata는 Gemini parser만 채우므로 Codex/Claude 출력은 바뀌지 않는다. After: ```go metadata := status.GetMetadata() if used := usageStatusMetadata(metadata, "used_percent"); used != "" { fmt.Fprintf(out, "Used: %s\n", used) } if limit := usageStatusMetadata(metadata, "usage_limit"); limit != "" { fmt.Fprintf(out, "Usage limit: %s\n", limit) } ``` #### 수정 파일 및 체크리스트 - [ ] `apps/edge/cmd/edge/console.go` - metadata lookup helper 추가 - [ ] `apps/edge/cmd/edge/console.go` - `formatUsageStatus`에서 `used_percent`, `usage_limit` 출력 - [ ] `apps/edge/cmd/edge/console_test.go` - `TestFormatUsageStatus_PrintsGeminiQuotaMetadata` 추가 - [ ] `apps/edge/cmd/edge/console_test.go` - 기존 Codex/Claude output tests가 변하지 않는지 확인 #### 테스트 작성 - `apps/edge/cmd/edge/console_test.go` - `TestFormatUsageStatus_PrintsGeminiQuotaMetadata`: `DailyLimit: "100%"`, `DailyResetTime: "24h"`, metadata `daily_label: "Daily quota"`, `used_percent: "0%"`, `usage_limit: "200"`에서 output이 `Daily quota: 100% remaining (resets 24h)`, `Used: 0%`, `Usage limit: 200`을 포함하는지 검증한다. #### 중간 검증 ```bash go test ./apps/edge/cmd/edge/... -run 'TestFormatUsageStatus' -count=1 ``` 기대 결과: 기존 Codex/Claude formatter tests와 새 Gemini formatter test가 모두 통과한다. ### [API-4] Bin shell smoke를 위한 edge config override 추가 #### 문제 `bin/edge.sh:1-8`은 항상 `configs/edge.yaml`만 사용한다. 기본 `configs/edge.yaml:20`의 `console.target`은 `"claude"`라서, 사용자가 요구한 bin shell 기준 Gemini `/status` smoke를 재현하려면 tracked config를 직접 바꿔야 한다. 계획 범위상 기본 config 변경은 피해야 하므로, bin smoke용 임시 config를 주입할 작은 override가 필요하다. Before (`bin/edge.sh:1`): ```bash #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_FILE="$REPO_ROOT/configs/edge.yaml" cd "$REPO_ROOT" echo "[edge] config=$CONFIG_FILE" exec go run ./apps/edge/cmd/edge console --config "$CONFIG_FILE" ``` #### 해결 방법 `bin/node.sh`가 `IOP_EDGE_ADDR`를 허용하듯, `bin/edge.sh`도 `IOP_EDGE_CONFIG`가 있으면 그 파일을 사용하고 없으면 기존 config를 유지하게 한다. 이 변경은 기본 실행 동작을 바꾸지 않는다. After: ```bash CONFIG_FILE="${IOP_EDGE_CONFIG:-$REPO_ROOT/configs/edge.yaml}" ``` 최종 smoke는 `/tmp`에 fake Gemini와 임시 edge config를 만들고, `IOP_EDGE_CONFIG=`로 `./bin/edge.sh`, `IOP_EDGE_ADDR=127.0.0.1:19090`로 `./bin/node.sh`를 실행한다. #### 수정 파일 및 체크리스트 - [ ] `bin/edge.sh` - `IOP_EDGE_CONFIG` override 추가 - [ ] 최종 검증 script - `/tmp` fake Gemini와 `/tmp` edge config로 `target=gemini` `/status` smoke 실행 #### 테스트 작성 별도 unit test는 작성하지 않는다. 변경은 1줄 shell fallback이고, 최종 bin smoke가 override 사용 여부를 검증한다. #### 중간 검증 ```bash IOP_EDGE_CONFIG=/tmp/non-existent-iop-edge.yaml ./bin/edge.sh ``` 기대 결과: 파일이 없어서 `go run ./apps/edge/cmd/edge console --config /tmp/non-existent-iop-edge.yaml` 경로의 config load error가 발생한다. 즉 `bin/edge.sh`가 `IOP_EDGE_CONFIG` 값을 실제로 전달한다. 이 명령은 실패가 기대 결과이므로, 구현 에이전트는 실제 stdout/stderr와 exit status를 `CODE_REVIEW-cloud-G07.md`에 기록한다. ## 수정 파일 요약 | 파일 | 항목 | |------|------| | `apps/node/internal/adapters/cli/status/parser.go` | API-1 | | `apps/node/internal/adapters/cli/status/parser_test.go` | API-1 | | `apps/node/internal/adapters/cli/status/gemini.go` | API-2 | | `apps/node/internal/adapters/cli/status/gemini_test.go` | API-2 | | `apps/node/internal/adapters/cli/status/status.go` | API-2 | | `apps/node/internal/adapters/cli/status/status_test.go` | API-2 | | `apps/edge/cmd/edge/console.go` | API-3 | | `apps/edge/cmd/edge/console_test.go` | API-3 | | `bin/edge.sh` | API-4 | ## 최종 검증 ```bash gofmt -l apps/node/internal/adapters/cli/status/parser.go apps/node/internal/adapters/cli/status/parser_test.go apps/node/internal/adapters/cli/status/gemini.go apps/node/internal/adapters/cli/status/gemini_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 ``` 기대 결과: 출력이 없어야 한다. ```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\(\)' apps/node/internal/adapters/cli/status; then exit 1; fi ``` 기대 결과: 출력 없이 exit 0이어야 한다. `NewGeminiChecker(command string)` 선언은 이 pattern에 걸리지 않아야 한다. ```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:19090"/; s/port: 9092/port: 19092/; s/target: "claude"/target: "gemini"/; s#command: "gemini"#command: "'"$tmpdir"'/gemini"#' configs/edge.yaml > "$tmpdir/edge.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_EDGE_ADDR=127.0.0.1:19090 ./bin/node.sh > "$tmpdir/node.out" 2> "$tmpdir/node.err" & node_pid=$! sleep 8 printf '/status\n/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" ``` 기대 결과: edge output에 `[node-local-node-status] target=gemini session=default`, `Daily quota: 100% remaining (resets 24h)`, `Used: 0%`, `Usage limit: 200`이 포함된다. `node.out`에는 edge reachable 이후 node serve 로그가 있어야 하고, fake Gemini가 `/stats model` 외 명령을 받았다는 `unexpected command`가 없어야 한다. 임시 도구는 `/tmp` 아래에만 남고 repo에는 생성하지 않는다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.