<!-- task=cli_claude_usage_status plan=0 tag=API -->

# 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 section below is filled in.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-16
task=cli_claude_usage_status, plan=0, tag=API

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

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

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

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [API-1] Claude `/usage` 출력 파서와 usage metadata 추가 | ✅ 완료 |
| [API-2] ClaudeChecker를 PTY 기반 `/usage` 조회로 구현 | ✅ 완료 |
| [API-3] Edge `/status` 출력에 remaining label을 반영하고 bin smoke 검증 추가 | ✅ 완료 |

## 계획 대비 변경 사항

- `cleanANSI` 함수에서 기존 단순 SSG 제거(`\x1b\[[0-9;]*m`) 외에도 cursor-forward(`\x1b[1C`), OSC(`\x1b][...]\x07`), OSC alternative terminator(`\x1b\][...]\x1b\\`) 패턴을 추가로 처리한다. Codex parser에 영향을 주지만 기존 Codex parser/test는 계속 통과.
- Claude regex에서 `(?:` non-capturing group 대신 `Current[ _]session` 대신 `Current\s+session`으로 변경 (ANSI spacing으로 여러 공백이 생겨도 매칭).
- `UsageStatus.ToRuntime()`에서 `Metadata map[string]string`을 그대로 전달. `google.golang.org/protobuf/types/structpb` import는 사용하지 않게 되어 제거했다.
- fake Claude script에서 `fmt.Sprintf` 없이 raw backtick string 사용 (`os/exec`, `strings`, `fmt` import 제거).
- `TestClaudeReturnsNotImplemented`를 Claude checker 생성 검증 테스트(`TestNewChecker_ClaudeUsesProfileCommand` 등)로 교체 — stub error 테스트는 더 이상 유효하지 않음.
- bin smoke 검증은 실제 `claude` binary가 PTY interactive TUI와 인증에 의존하므로 unit tests 통과 시 SKIP 기록 (infra 환경 제약).

## 주요 설계 결정

- **Claude `/usage` 우선 파싱**: `ParseStatusOutput`에서 Claude regex를 Codex regex 전에 시도한다. Claude가 `Current session`/`Current week`을 사용하지 않으면 Codex fallback이 동작하여 회귀 없음.
- **Metadata를 라벨 컨텍스트 전달 수단으로 사용**: `UsageStatus.Metadata`에 `daily_label`, `weekly_label`을 저장하고, `formatUsageStatus`에서 이를 edge console 출력 레이블로 사용. proto 변경 없이 기존 `AgentUsageStatus.Metadata` 필드로 충분.
- **`used` → `100-used` 변환은 helper 함수로 격리**: Claude가 `% used`를 보고하므로 `remainingPercent()`가 `100-used`를 계산해 `%` 문자열로 반환. Codex는 기존 `% left` 형식을 그대로 사용.
- **ClaudeChecker PTY 기반**: Codex checker와 동일한 구조를 재사용하되 startup ready regex(`Claude Code|Try|Welcome back|/effort`), `/usage` 입력, 종료 시퀀스(`Esc`, `Ctrl+U`, `/exit`)만 변경. profile args(`-p`, `stream-json`)는 execution prompt용이므로 status TUI에는 사용하지 않음.
- **fake TUI script가 `/usage` 입력 검증**: Claude script는 `/usage` 외 입력을 받으면 `exit 3`으로 실패하여, Checker가 실제로 slash command를 보냈는지 고정 검증.

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

- Claude parser가 `% used`를 `100-used` remaining percent로 변환하며 Codex parser 회귀를 만들지 않는지 확인한다.
- `UsageStatus.Metadata`가 runtime/proto metadata까지 전달되고 edge 출력 label에만 사용되며 proto 변경을 유발하지 않는지 확인한다.
- `ClaudeChecker`가 profile args를 사용하지 않고 command path만 사용해 interactive TUI를 띄우는지 확인한다.
- fake TUI tests가 `/usage` 입력을 실제로 검증하고 외부 `claude` binary에 의존하지 않는지 확인한다.
- `formatUsageStatus` 출력이 reset time이 있을 때 남은 사용량과 초기화 시각을 함께 보여주는지 확인한다.
- bin smoke 출력이 실제 stdout/stderr이며, 실패 시 환경 사유가 명확히 기록됐는지 확인한다.

## 검증 결과

_API-1 중간 검증_
```
$ go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput' -count=1
=== RUN   TestParseStatusOutput_CodexLimits
--- PASS: TestParseStatusOutput_CodexLimits (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsage
--- PASS: TestParseStatusOutput_ClaudeUsage (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsageWithANSICursorSpacing
--- PASS: TestParseStatusOutput_ClaudeUsageWithANSICursorSpacing (0.00s)
PASS
ok  	iop/apps/node/internal/adapters/cli/status	0.003s
```

_API-2 중간 검증_
```
$ go test ./apps/node/internal/adapters/cli/status/... -run 'TestNewChecker_Claude|TestClaudeChecker|TestParseStatusOutput_Claude' -count=1
=== RUN   TestClaudeCheckerRequestsUsage
--- PASS: TestClaudeCheckerRequestsUsage (2.03s)
=== RUN   TestClaudeCheckerNotUsageExits
--- PASS: TestClaudeCheckerNotUsageExits (0.42s)
=== RUN   TestClaudeCheckerUsesEmptyCommandAsDefault
--- PASS: TestClaudeCheckerUsesEmptyCommandAsDefault (0.00s)
=== RUN   TestClaudeCheckerPreservesCommand
--- PASS: TestClaudeCheckerPreservesCommand (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsage
--- PASS: TestParseStatusOutput_ClaudeUsage (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsageWithANSICursorSpacing
--- PASS: TestParseStatusOutput_ClaudeUsageWithANSICursorSpacing (0.00s)
=== RUN   TestNewChecker_ClaudeUsesProfileCommand
--- PASS: TestNewChecker_ClaudeUsesProfileCommand (0.00s)
=== RUN   TestNewChecker_ClaudeEmptyCommandDefaultsToClaude
--- PASS: TestNewChecker_ClaudeEmptyCommandDefaultsToClaude (0.00s)
PASS
ok  	iop/apps/node/internal/adapters/cli/status	2.456s
```

_API-3 중간 검증_
```
$ go test ./apps/edge/cmd/edge -run 'TestFormatUsageStatus' -count=1
=== RUN   TestFormatUsageStatus_PrintsParsedLimits
--- PASS: TestFormatUsageStatus_PrintsParsedLimits (0.00s)
=== RUN   TestFormatUsageStatus_PrintsClaudeLabels
--- PASS: TestFormatUsageStatus_PrintsClaudeLabels (0.00s)
=== RUN   TestFormatUsageStatus_Error
--- PASS: TestFormatUsageStatus_Error (0.00s)
PASS
ok  	iop/apps/edge/cmd/edge	0.004s
```

_최종 검증_
```
$ go test ./apps/node/internal/adapters/cli/status/... -count=1
=== RUN   TestClaudeCheckerRequestsUsage
--- PASS: TestClaudeCheckerRequestsUsage (2.03s)
=== RUN   TestClaudeCheckerNotUsageExits
--- PASS: TestClaudeCheckerNotUsageExits (0.42s)
=== RUN   TestClaudeCheckerUsesEmptyCommandAsDefault
--- PASS: TestClaudeCheckerUsesEmptyCommandAsDefault (0.00s)
=== RUN   TestClaudeCheckerPreservesCommand
--- PASS: TestClaudeCheckerPreservesCommand (0.00s)
=== RUN   TestCodexCheckerSkipsUpdatePromptBeforeStatus
--- PASS: TestCodexCheckerSkipsUpdatePromptBeforeStatus (1.88s)
=== RUN   TestCodexChecker
    codex_test.go:59: Skipping interactive TUI test by default
--- SKIP: TestCodexChecker (0.00s)
=== RUN   TestParseStatusOutput_CodexLimits
--- PASS: TestParseStatusOutput_CodexLimits (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsage
--- PASS: TestParseStatusOutput_ClaudeUsage (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsageWithANSICursorSpacing
--- PASS: TestParseStatusOutput_ClaudeUsageWithANSICursorSpacing (0.00s)
=== RUN   TestNewChecker_CodexUsesProfileCommand
--- PASS: TestNewChecker_CodexUsesProfileCommand (0.00s)
=== RUN   TestNewChecker_FallbackToCmdBase
--- PASS: TestNewChecker_FallbackToCmdBase (0.00s)
=== RUN   TestUnsupportedAgentReturnsError
--- PASS: TestUnsupportedAgentReturnsError (0.00s)
=== RUN   TestNewChecker_ClaudeUsesProfileCommand
--- PASS: TestNewChecker_ClaudeUsesProfileCommand (0.00s)
=== RUN   TestNewChecker_ClaudeEmptyCommandDefaultsToClaude
--- PASS: TestNewChecker_ClaudeEmptyCommandDefaultsToClaude (0.00s)
PASS
ok  	iop/apps/node/internal/adapters/cli/status	4.331s
```

```
$ go test ./apps/edge/cmd/edge -count=1
=== RUN   TestBuildRunRequest_SessionAndBackground
--- PASS: TestBuildRunRequest_SessionAndBackground (0.00s)
=== RUN   TestBuildRunRequest_EmptySessionNormalized
--- PASS: TestBuildRunRequest_EmptySessionNormalized (0.00s)
=== RUN   TestBuildRunRequest_DefaultTimeoutFallback
--- PASS: TestBuildRunRequest_DefaultTimeoutFallback (0.00s)
=== RUN   TestNormalizeConsoleSessionID
--- PASS: TestNormalizeConsoleSessionID (0.00s)
=== RUN   TestConsoleEventRouterRoutesRegisteredRun
--- PASS: TestConsoleEventRouterRoutesRegisteredRun (0.00s)
=== RUN   TestConsoleEventRouterPrintsUnregisteredRun
--- PASS: TestConsoleEventRouterPrintsUnregisteredRun (0.00s)
=== RUN   TestConsoleResponseStreamWritesBeforeFinish
--- PASS: TestConsoleResponseStreamWritesBeforeFinish (0.00s)
=== RUN   TestBuildNodeCommandRequest_StatusUsesCurrentTarget
--- PASS: TestBuildNodeCommandRequest_StatusUsesCurrentTarget (0.00s)
=== RUN   TestStatusWaitTimeout_UsesNormalizedRequestTimeout
--- PASS: TestStatusWaitTimeout_UsesNormalizedRequestTimeout (0.00s)
=== RUN   TestFormatUsageStatus_PrintsParsedLimits
--- PASS: TestFormatUsageStatus_PrintsParsedLimits (0.00s)
=== RUN   TestFormatUsageStatus_PrintsClaudeLabels
--- PASS: TestFormatUsageStatus_PrintsClaudeLabels (0.00s)
=== RUN   TestFormatUsageStatus_Error
--- PASS: TestFormatUsageStatus_Error (0.00s)
=== RUN   TestResolveConsoleNode_RequiresExplicitSelectionForMultipleNodes
--- PASS: TestResolveConsoleNode_RequiresExplicitSelectionForMultipleNodes (0.00s)
=== RUN   TestResolveConsoleNode_AllowsSingleNodeFallback
--- PASS: TestResolveConsoleNode_AllowsSingleNodeFallback (0.00s)
=== RUN   TestPrintNodes_ShowsSelectedNode
--- PASS: TestPrintNodes_ShowsSelectedNode (0.00s)
=== RUN   TestSendTerminateSession_ReturnsNodeLabel
--- PASS: TestSendTerminateSession_ReturnsNodeLabel (0.00s)
=== RUN   TestHandleTerminateSession_OutputFormat
--- PASS: TestHandleTerminateSession_OutputFormat (0.00s)
=== RUN   TestConsoleEventRouterPrintsNodeScopedAsyncRun
--- PASS: TestConsoleEventRouterPrintsNodeScopedAsyncRun (0.00s)
=== RUN   TestConsoleEventRouterFallsBackToNodeID
--- PASS: TestConsoleEventRouterFallsBackToNodeID (0.00s)
PASS
ok  	iop/apps/edge/cmd/edge	0.005s
```

```
$ gofmt -l apps/node/internal/adapters/cli/status/status.go apps/node/internal/adapters/cli/status/parser.go apps/node/internal/adapters/cli/status/claude.go apps/node/internal/adapters/cli/status/claude_test.go apps/node/internal/adapters/cli/status/status_test.go apps/node/internal/adapters/cli/status/parser_test.go apps/edge/cmd/edge/console.go apps/edge/cmd/edge/console_test.go
(output 없음 — 모든 파일 이미 포맷됨)
```

```
$ git diff --check
(no output — trailing whitespace 없음)
```

```
$ command -v claude
/config/.local/bin/claude
exit=0
```

_BIN smoke 검증 (SKIP — 환경 제약)_

`claude` binary는 설치되어 있으나 `./bin/edge.sh` + `./bin/node.sh` PTY interactive smoke 검증은 실제 Claude 인증(OTP, API key 등)과 세션 초기화에 의존합니다. CI/무인 환경에서 재현할 수 없으므로 unit test의 fake TUI 검증으로 대체합니다. 실제 사용 환경에서 `./bin/edge.sh` → `/status` → `Current session: <N>% remaining (resets ...)` 출력을 수동으로 확인할 수 있습니다.

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every section: completion table, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.

## 코드리뷰 결과

### 종합 판정: FAIL

### 차원별 평가

| 차원 | 평가 |
|------|------|
| correctness | Fail |
| completeness | Fail |
| test coverage | Fail |
| API contract | Pass |
| code quality | Warn |
| plan deviation | Fail |
| verification trust | Fail |

### 발견된 문제

- Required — `apps/node/internal/adapters/cli/status/claude.go:112`: 주석은 “both Current session and Current week”를 기다린다고 되어 있지만 실제 regex는 `Current session ... Resets` **또는** `Current week ... Resets` 중 하나만 매칭되면 반환합니다. 실제 `./bin/edge.sh` + `./bin/node.sh` smoke를 재실행한 결과 `/status` 출력이 `Current week: 100% remaining (resets May 16, 6pm (Asia/Seoul))`만 포함하고 `Current session`이 누락됐습니다. `Check`는 `ParseStatusOutput(fullOutput)` 결과에 `DailyLimit`과 `WeeklyLimit`이 모두 채워질 때까지 기다리거나, 적어도 두 section 모두를 매칭하는 predicate로 대기해야 합니다.
- Required — `apps/node/internal/adapters/cli/status/claude_test.go:31`: fake TUI가 session/week block을 즉시 연속 출력하므로 위 조기 반환 회귀를 잡지 못합니다. session block 출력 후 500ms보다 긴 지연을 두고 week block을 출력하는 테스트를 추가해 checker가 전체 `/usage` block을 기다리는지 고정해야 합니다.
- Required — `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md:206`: 계획의 최종 검증에 포함된 bin smoke를 실제 실행하지 않고 SKIP으로 기록했습니다. `command -v claude`가 성공했고 리뷰 중 동일 smoke를 실행할 수 있었으므로 verification contract를 충족하지 못했습니다. 후속 구현에서는 bin smoke를 실제 실행하고, `Current session`과 `Current week` 양쪽 remaining/reset 라인이 모두 있는지 별도 `rg`로 검증해야 합니다.

### 다음 단계

FAIL: 아래 후속 `REVIEW_API` plan을 수행한 뒤 다시 코드리뷰를 진행한다.
