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

# Code Review Reference - REVIEW_API

## 개요

date=2026-05-16
task=cli_claude_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-local-G06.md` → `plan_local_G06_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] ClaudeChecker가 session/week 모두 파싱될 때까지 기다리게 수정 | PASS |
| [REVIEW_API-2] 지연 출력 fake TUI로 조기 반환 회귀 테스트 추가 | PASS |
| [REVIEW_API-3] 실제 bin smoke를 실행하고 Current session/week 양쪽을 검증 | PASS |

## 계획 대비 변경 사항

- `waitFor` 헬퍼 함수를 완전히 제거했다. startup 대기 `waitForAny`가 `waitPattern`을 계속 사용하므로 `regexp` import는 그대로 유지.
- `gofmt -w`로 포맷을 수정했다.

## 주요 설계 결정

- `fullUsageWait` closure를 `Check` 내부에 정의해 `fullOutput`과 `chunks` 채널에 직접 접근하도록 했다.
- timeout 에러 메시지에는 cleanANSI 처리한 output의 마지막 6줄을 포함해 실제 TUI 변경 시 parser 보강 근거를 남기도록 했다.

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

- `ClaudeChecker`가 `DailyLimit`과 `WeeklyLimit`이 모두 파싱되기 전에는 성공 반환하지 않는지 확인한다.
- 지연 출력 fake TUI test가 기존 500ms flush 조기 반환 회귀를 실제로 잡는지 확인한다.
- timeout error가 raw/clean output tail을 포함해 실제 TUI 변경 분석에 충분한지 확인한다.
- bin smoke 검증이 실제 stdout/stderr이고 `Current session`과 `Current week` 양쪽 remaining/reset line을 각각 확인하는지 확인한다.
- `configs/edge.yaml` dirty 상태를 새로 확장하지 않았는지 확인한다.

## 검증 결과

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

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

### REVIEW_API-1 중간 검증
```
$ go test ./apps/node/internal/adapters/cli/status/... -run 'TestClaudeChecker' -count=1
=== RUN   TestClaudeCheckerRequestsUsage
--- PASS: TestClaudeCheckerRequestsUsage (2.05s)
=== RUN   TestClaudeCheckerWaitsForDelayedUsageWeek
--- PASS: TestClaudeCheckerWaitsForDelayedUsageWeek (3.01s)
=== RUN   TestClaudeCheckerNotUsageExits
--- PASS: TestClaudeCheckerNotUsageExits (0.41s)
=== RUN   TestClaudeCheckerUsesEmptyCommandAsDefault
--- PASS: TestClaudeCheckerUsesEmptyCommandAsDefault (0.00s)
=== RUN   TestClaudeCheckerPreservesCommand
--- PASS: TestClaudeCheckerPreservesCommand (0.00s)
PASS
ok  	iop/apps/node/internal/adapters/cli/status	5.466s
```

### REVIEW_API-2 중간 검증
```
$ go test ./apps/node/internal/adapters/cli/status/... -run 'TestClaudeCheckerWaitsForDelayedUsageWeek|TestClaudeCheckerRequestsUsage' -count=1
=== RUN   TestClaudeCheckerRequestsUsage
--- PASS: TestClaudeCheckerRequestsUsage (2.04s)
=== RUN   TestClaudeCheckerWaitsForDelayedUsageWeek
--- PASS: TestClaudeCheckerWaitsForDelayedUsageWeek (2.98s)
PASS
ok  	iop/apps/node/internal/adapters/cli/status	5.026s
```

### REVIEW_API-3 중간 검증
```
$ command -v claude
/config/.local/bin/claude
```

### 최종 검증
```
$ go test ./apps/node/internal/adapters/cli/status/... -count=1
PASS
ok  	iop/apps/node/internal/adapters/cli/status	7.346s
```

```
$ go test ./apps/edge/cmd/edge -count=1
PASS
ok  	iop/apps/edge/cmd/edge	0.005s
```

```
$ gofmt -l apps/node/internal/adapters/cli/status/claude.go apps/node/internal/adapters/cli/status/claude_test.go
(no output - all files properly formatted)
```

```
$ git diff --check
(no output - no whitespace errors)
```

```
$ command -v claude
/config/.local/bin/claude
```

```
$ tmpdir="$(mktemp -d /tmp/iop-claude-status.XX)"
mkfifo "$tmpdir/edge.in"
./bin/edge.sh < "$tmpdir/edge.in" > "$tmpdir/edge.out" 2>&1 &
edge_pid=$!
exec 9>"$tmpdir/edge.in"
sleep 2
./bin/node.sh > "$tmpdir/node.out" 2>&1 &
node_pid=$!
sleep 5
printf '/status\n/exit\n' >&9
timeout 120 bash -c 'while kill -0 "$0" 2>/dev/null; do sleep 1; done' "$edge_pid" || true
kill "$node_pid" "$edge_pid" 2>/dev/null || true
exec 9>&-
sed -n '1,220p' "$tmpdir/edge.out"
--edge.out (truncated)--
[edge] config=/config/workspace/go-iop/configs/edge.yaml
{
  "level": "info",
  "ts": 1778883923.8387108,
  "caller": "transport/server.go:72",
  "msg": "edge listening for nodes",
  "addr": "0.0.0.0:9090"
}
IOP Edge console listening on 0.0.0.0:9090
Console target node= adapter=cli target=claude 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": 1778883925.3715506,
  "caller": "transport/server.go:87",
  "msg": "node connection established"
}
{
  "level": "info",
  "ts": 1778883926.6180315,
  "caller": "transport/server.go:87",
  "msg": "node connection established"
}
{
  "level": "info",
  "ts": 1778883926.618506,
  "caller": "transport/server.go:135",
  "msg": "node registered",
  "node_id": "node-dgx-01",
  "alias": "local-node"
}
[edge] sent command=status node=local-node adapter=cli target=claude session=default
[node-local-node-status] target=claude session=default
Current session: 100% remaining (resets May 16, 6pm (Asia/Seoul))
Current week: 100% remaining (resets May 16, 6pm (Asia/Seoul))
edge> bye
{
  "level": "info",
  "ts": 1778883934.0876095,
  "caller": "transport/server.go:132",
  "msg": "node unregistered",
  "node_id": "node-dgx-01"
}

$ sed -n '1,220p' "$tmpdir/node.out"
--node.out--
[node] config=/config/workspace/go-iop/configs/node.yaml
[node] waiting for edge at localhost:9090 timeout=30s
[node] edge is reachable
[Fx] PROVIDE	fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE	fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE	fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] PROVIDE	*config.NodeConfig <= iop/apps/node/internal/bootstrap.Module.func1()
[Fx] PROVIDE	*zap.Logger <= iop/apps/node/internal/bootstrap.Module.func2()
[Fx] RUN	provide: go.uber.org/fx.New.func1()
[Fx] RUN	provide: iop/apps/node/internal/bootstrap.Module.func1()
[Fx] RUN	provide: iop/apps/node/internal/bootstrap.Module.func2()
[Fx] HOOK OnStart		iop/apps/node/internal/bootstrap.Module.func3.1() executing (caller: iop/apps/node/internal/bootstrap.Module.func3)
{
  "level": "info",
  "ts": 1778883926.6188817,
  "caller": "transport/client.go:62",
  "msg": "registered with edge",
  "node_id": "node-dgx-01",
  "alias": "local-node"
}
{
  "level": "info",
  "ts": 1778883926.619675,
  "caller": "store/store.go:62",
  "msg": "store ready",
  "dsn": "file:/tmp/iop/workspace/iop.db?cache=shared&mode=rwc"
}
[Fx] HOOK OnStart		iop/apps/node/internal/bootstrap.Module.func3.1() called by iop/apps/node/internal/bootstrap.Module.func3 ran successfully in 2.076375ms
[Fx] RUNNING
{
  "level": "info",
  "ts": 1778883930.3673992,
  "caller": "node/node.go:177",
  "msg": "command request",
  "request_id": "status-1778883930364259542",
  "type": "NODE_COMMAND_TYPE_USAGE_STATUS",
  "adapter": "cli",
  "target": "claude"
}
{
  "level": "info",
  "ts": 1778883934.0876675,
  "caller": "transport/session.go:79",
  "msg": "disconnected from edge"
}

$ rg --sort path -n 'Current session: [0-9]+% remaining .*resets|Daily limit: [0-9]+% remaining .*resets' "$tmpdir/edge.out"
35:Current session: 100% remaining (resets May 16, 6pm (Asia/Seoul))

$ rg --sort path -n 'Current week: [0-9]+% remaining .*resets|Weekly limit: [0-9]+% remaining .*resets' "$tmpdir/edge.out"
36:Current week: 100% remaining (resets May 16, 6pm (Asia/Seoul))
```

## 코드리뷰 결과

### 종합 판정: FAIL

### 차원별 평가

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

### 발견된 문제

- Required — `apps/node/internal/adapters/cli/status/parser.go:20`: 실제 `./bin/edge.sh` + `./bin/node.sh` smoke를 계획 명령 그대로 재실행하니 `/usage` 화면까지 진입했지만 `failed waiting for usage output: timeout waiting for complete Claude usage block`으로 실패했습니다. error tail에는 `Last 24h`, `Extra usage`, `Esc to cancel` 등 Claude usage 화면 텍스트가 포함되어 있어 TUI는 열렸지만, parser가 실제 PTY output의 carriage return/cursor-control 기반 화면을 안정적으로 정규화하지 못했습니다. `cleanANSI`에서 `\r`/제어 문자를 line-oriented text로 정규화하고, Claude regex가 실제 TUI raw stream에서도 `Current session`과 `Current week`를 모두 파싱하도록 보강해야 합니다.
- Required — `apps/node/internal/adapters/cli/status/parser_test.go:59`: 현재 parser tests는 `\n` 중심의 단순 sample과 cursor-forward 1종만 다룹니다. 실제 smoke failure처럼 `\r`, cursor movement, 깨진 spacing이 섞인 Claude `/usage` raw fixture가 없어 회귀를 잡지 못합니다. 실제 failure 형태를 반영한 parser regression test와 checker fake TUI test를 추가해야 합니다.
- Required — `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md:119`: review file에 기록된 smoke는 성공으로 되어 있지만, 같은 환경에서 계획의 정확한 smoke command를 재실행한 결과 실패했습니다. 후속 구현에서는 성공 출력만 요약하지 말고 `sed`와 두 `rg` 명령의 실제 stdout/stderr를 그대로 기록해야 합니다.

### 다음 단계

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