<!-- task=cli_claude_usage_status plan=4 tag=REVIEW_REVIEW_REVIEW_REVIEW_API -->

# Code Review Reference - REVIEW_REVIEW_REVIEW_REVIEW_API

## 개요

date=2026-05-16
task=cli_claude_usage_status, plan=4, tag=REVIEW_REVIEW_REVIEW_REVIEW_API

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

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

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

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REVIEW_REVIEW_REVIEW_API-1] 실제 실패 화면 full state 진단 | [x] |
| [REVIEW_REVIEW_REVIEW_REVIEW_API-2] Claude `/usage` TUI state machine 수정 | [x] |
| [REVIEW_REVIEW_REVIEW_REVIEW_API-3] 현재 실패 형태 regression tests 추가 | [x] |
| [REVIEW_REVIEW_REVIEW_REVIEW_API-4] 2회 연속 smoke 검증 신뢰성 회복 | [x] |

## 계획 대비 변경 사항

- repo 밖 `/tmp`에 진단용 Go 프로그램 두 개를 만들어 실제 `claude` PTY raw stream을 캡처하고 `cleanANSI` 후의 모습을 분석했다. 진단 코드는 모두 삭제했고 `find . -maxdepth 3 (-name '*diag*' -o -name '*raw*' -o -name '*probe*')` 출력이 비어 있음을 확인했다. 진단 결과는 아래 `주요 설계 결정`에 반영했다.
- `/usage` 패널은 실제로 정상적으로 열리고 "Current session" / "Current week (all models)" 헤더와 데이터가 모두 raw stream에 도착한다 (실측: `/usage` 송신 1초 안에 둘 다 등장). 따라서 plan에서 가설로 제시한 "view navigation으로 데이터를 끌어와야 하는 상태"는 실제 default 상태가 아니다. 그러나 review-fail 시 보고된 timeout tail (`d to day · w to week`, `Extra usage`, `Esc to cancel`)은 **panel 후반부** 텍스트라서, 그 텍스트가 보였다는 것은 panel이 열렸음에도 parser가 `Current session`/`Current week` 페이로드를 매칭하지 못했다는 뜻이었다. 그 근본 원인은 G07 parser의 lazy regex가 section을 넘어가서 잘못된 매치/논매치를 일으킨 점이다 (예: G07 smoke에서 `Current session: 98% remaining (resets May 16, 6pm (Asia/Seoul))`로 출력된 것 — May 16 6pm은 week의 reset인데 session에 붙었다). 그래서 1차 fix는 **parser를 section-aware로 재작성**한 것이고, 그 위에 추가 안전망으로 day/week pane nudge(`d`/`w`)를 보내는 state machine을 얹었다.
- `d`/`w` nudge는 plan이 요구한 "view navigation"을 만족하면서 동시에 panel이 어떤 사정으로 데이터를 늦게 그릴 때도 화면을 한 번 더 repaint시키는 안전망 역할을 한다. nudge는 3초/6초 시점에 한 번씩 idempotent하게 보낸다.
- 진단 결과 timeout 단순 증가만으로는 문제가 안 풀린다는 점이 명확해서, timeout은 60s를 유지하되 fix의 본질은 parser 알고리즘 변경에 두었다.

## 주요 설계 결정

- **Parser: section-aware multi-occurrence matching**. `parser.go`에서 단일 `(?is)Current\s+session.*?\d+%\s+used\s+Resets\s+([^\n]+)` 식의 cross-section lazy 매치를 버리고, 세 개 regex로 분리했다.
  - `claudeSessionHeaderRegex` / `claudeWeekHeaderRegex`: 각 섹션 헤더의 모든 발생 위치를 `FindAllStringIndex`로 수집.
  - `claudePayloadRegex`: section slice 내부에서만 `(\d+)% used … Resets <line>` 한 쌍을 추출.
  - 추출 로직은 헤더 발생을 last-to-first로 순회하면서 (loading repaint 후의 data repaint를 우선) 다음 opposing 헤더 전까지를 slice 경계로 사용한다. 이 알고리즘으로 G07에서 발생한 "session header에 week payload 붙음" 회귀가 구조적으로 불가능해진다.
- **Checker: 두 단계 wait + idempotent nudge**.
  - `usagePanelRegex` (`Current session|Current week|Esc to cancel|d to day`)로 "패널이 열렸음"을 먼저 확인 — `/usage` 입력 실패와 데이터 미수신을 error tail에서 구분할 수 있다.
  - `fullUsageWait`은 3초 시점에 `d`, 6초 시점에 `w`를 한 번씩 보낸다. nudge는 panel의 "d to day · w to week" 힌트가 광고하는 그대로의 idempotent 동작이고, 데이터가 이미 도착해 parser가 성공한 상태라면 nudge timer가 발화하기 전에 함수가 리턴한다.
  - timeout error tail은 12→40줄로 넓혀서 다음 리뷰 회차가 panel state를 직접 보고 판단할 수 있게 했다.
- **Test: real-state regression**.
  - `TestParseStatusOutput_ClaudeUsageCurrentRealState`: cursor positioning으로 raw stream이 `session header → week header → week payload → session payload` 순으로 interleave된 상태를 재현한다. G07 parser는 session에 week reset을 붙였지만, 새 parser는 section을 넘지 않으므로 session payload 슬롯이 비고, week payload는 week에 정확히 매칭된다.
  - `TestClaudeCheckerNavigatesUsageDayWeekViews`: bash fake가 `/usage` 직후엔 헤더만 있고 payload 없는 panel을 그린 뒤, `dd bs=1 count=1` (PTY는 `stty -icanon` raw로 전환)로 checker의 `d` keystroke를 기다린다. nudge가 도착해야 비로소 실제 payload를 emit하므로, 단순 timeout-기반 wait만 있는 구현은 통과할 수 없다.
- 진단 artifact는 모두 `/tmp` 안 또는 repo 내 임시 폴더에 만들었다가 삭제했다. 최종 변경에는 임시 print, hard-coded reset time, 실제 계정값 fixture가 없다. fixture 값은 synthetic (`4% used / Resets 10am (Asia/Seoul)`, `0% used / Resets May 16, 6pm (Asia/Seoul)`).
- Codex parser (`5h limit` / `Weekly limit`) 와 `parseCodexUsage`는 손대지 않았다. `TestParseStatusOutput_CodexLimits`가 그대로 통과한다.

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

- 실제 실패 state가 full raw/sanitized/visible output 기준으로 진단됐는지 확인한다.
- 단순 timeout 증가가 아니라 `/usage` TUI state machine 또는 parser 입력이 실제 day/week/session view에 맞게 바뀌었는지 확인한다.
- 새 tests가 `d to day · w to week`, `Extra usage`, `Esc to cancel` failure state를 재현하는지 확인한다.
- Codex `5h limit`/`Weekly limit` parser와 edge 출력 포맷이 회귀하지 않았는지 확인한다.
- repo 안에 진단 artifact, raw dump, probe 코드, hard-coded 실제 reset time이 남지 않았는지 확인한다.
- 최종 smoke가 2회 연속 실행됐고 두 run 모두 negative error check와 positive `rg` checks를 통과했는지 확인한다.

## 검증 결과

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

### REVIEW_REVIEW_REVIEW_REVIEW_API-1 중간 검증
```
$ find . -maxdepth 3 \( -name '*diag*' -o -name '*raw*' -o -name '*probe*' \) -not -path './.git/*' -print
(빈 출력 — repo 안에 진단/probe artifact 없음)
```

### REVIEW_REVIEW_REVIEW_REVIEW_API-2 중간 검증
```
$ go test ./apps/node/internal/adapters/cli/status/... -run 'TestClaudeChecker|TestParseStatusOutput_Claude|TestParseStatusOutput_CodexLimits' -count=1 -v
=== RUN   TestClaudeCheckerRequestsUsage
--- PASS: TestClaudeCheckerRequestsUsage (2.55s)
=== RUN   TestClaudeCheckerWaitsForDelayedUsageWeek
--- PASS: TestClaudeCheckerWaitsForDelayedUsageWeek (3.49s)
=== RUN   TestClaudeCheckerSelectsUsageAndParsesRepaintedScreen
--- PASS: TestClaudeCheckerSelectsUsageAndParsesRepaintedScreen (3.49s)
=== RUN   TestClaudeCheckerNavigatesUsageDayWeekViews
--- PASS: TestClaudeCheckerNavigatesUsageDayWeekViews (5.59s)
=== RUN   TestClaudeCheckerNotUsageExits
--- PASS: TestClaudeCheckerNotUsageExits (0.92s)
=== RUN   TestClaudeCheckerUsesEmptyCommandAsDefault
--- PASS: TestClaudeCheckerUsesEmptyCommandAsDefault (0.00s)
=== RUN   TestClaudeCheckerPreservesCommand
--- PASS: TestClaudeCheckerPreservesCommand (0.00s)
=== RUN   TestClaudeCheckerParsesCarriageReturnUsageScreen
--- PASS: TestClaudeCheckerParsesCarriageReturnUsageScreen (2.55s)
=== 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   TestParseStatusOutput_ClaudeUsageWithCarriageReturns
--- PASS: TestParseStatusOutput_ClaudeUsageWithCarriageReturns (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsageFromRepaintedScreen
--- PASS: TestParseStatusOutput_ClaudeUsageFromRepaintedScreen (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsageCurrentRealState
--- PASS: TestParseStatusOutput_ClaudeUsageCurrentRealState (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsageWithTerminalControls
--- PASS: TestParseStatusOutput_ClaudeUsageWithTerminalControls (0.00s)
PASS
ok  	iop/apps/node/internal/adapters/cli/status	18.588s
```

### REVIEW_REVIEW_REVIEW_REVIEW_API-3 중간 검증
```
$ go test ./apps/node/internal/adapters/cli/status/... -run 'TestClaudeCheckerNavigatesUsageDayWeekViews|TestParseStatusOutput_ClaudeUsageCurrentRealState|TestParseStatusOutput_CodexLimits' -count=1 -v
=== RUN   TestClaudeCheckerNavigatesUsageDayWeekViews
--- PASS: TestClaudeCheckerNavigatesUsageDayWeekViews (5.60s)
=== RUN   TestParseStatusOutput_CodexLimits
--- PASS: TestParseStatusOutput_CodexLimits (0.00s)
=== RUN   TestParseStatusOutput_ClaudeUsageCurrentRealState
--- PASS: TestParseStatusOutput_ClaudeUsageCurrentRealState (0.00s)
PASS
ok  	iop/apps/node/internal/adapters/cli/status	5.605s
```

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

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

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

```
$ gofmt -l apps/node/internal/adapters/cli/status/parser.go apps/node/internal/adapters/cli/status/claude.go apps/node/internal/adapters/cli/status/parser_test.go apps/node/internal/adapters/cli/status/claude_test.go
(빈 출력 — 모든 파일 gofmt 통과)
```

```
$ git diff --check
(빈 출력, exit 0 — whitespace 이슈 없음)
```

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

```
$ set -euo pipefail
$ for run in 1 2; do ... done  # plan §최종 검증의 2회 연속 smoke 명령 전체

==================== run=1 tmpdir=/tmp/iop-claude-status.run1.N4xnBr ====================
--- EDGE.OUT (run 1) ---
[edge] config=/config/workspace/go-iop/configs/edge.yaml
{
  "level": "info",
  "ts": 1778891321.4260125,
  "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": 1778891322.921451,
  "caller": "transport/server.go:87",
  "msg": "node connection established"
}
{
  "level": "info",
  "ts": 1778891324.352685,
  "caller": "transport/server.go:87",
  "msg": "node connection established"
}
{
  "level": "info",
  "ts": 1778891324.3533332,
  "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: 67% remaining (resets 10am (Asia/Seoul))
Current week: 97% remaining (resets May 16, 6pm (Asia/Seoul))
edge> bye
{
  "level": "info",
  "ts": 1778891331.3793552,
  "caller": "transport/server.go:132",
  "msg": "node unregistered",
  "node_id": "node-dgx-01"
}
--- NEGATIVE (run 1) ---
(빈 매칭, ! 부정 후 exit=0 — negative check 통과)
--- EDGE STATUS (run 1) ---
33:[edge] sent command=status node=local-node adapter=cli target=claude session=default
--- NODE STATUS (run 1) ---
34:[node-local-node-status] target=claude session=default
--- SESSION/DAILY (run 1) ---
35:Current session: 67% remaining (resets 10am (Asia/Seoul))
--- WEEK/WEEKLY (run 1) ---
36:Current week: 97% remaining (resets May 16, 6pm (Asia/Seoul))

==================== run=2 tmpdir=/tmp/iop-claude-status.run2.flQuoe ====================
--- EDGE.OUT (run 2) ---
[edge] config=/config/workspace/go-iop/configs/edge.yaml
{
  "level": "info",
  "ts": 1778891333.457669,
  "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": 1778891334.98034,
  "caller": "transport/server.go:87",
  "msg": "node connection established"
}
{
  "level": "info",
  "ts": 1778891335.5143766,
  "caller": "transport/server.go:87",
  "msg": "node connection established"
}
{
  "level": "info",
  "ts": 1778891335.5150874,
  "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: 67% remaining (resets 10am (Asia/Seoul))
Current week: 97% remaining (resets May 16, 6pm (Asia/Seoul))
edge> bye
{
  "level": "info",
  "ts": 1778891343.4215071,
  "caller": "transport/server.go:132",
  "msg": "node unregistered",
  "node_id": "node-dgx-01"
}
--- NEGATIVE (run 2) ---
(빈 매칭, ! 부정 후 exit=0 — negative check 통과)
--- EDGE STATUS (run 2) ---
33:[edge] sent command=status node=local-node adapter=cli target=claude session=default
--- NODE STATUS (run 2) ---
34:[node-local-node-status] target=claude session=default
--- SESSION/DAILY (run 2) ---
35:Current session: 67% remaining (resets 10am (Asia/Seoul))
--- WEEK/WEEKLY (run 2) ---
36:Current week: 97% remaining (resets May 16, 6pm (Asia/Seoul))

==================== orphans ====================
$ ps -ef | rg './bin/(edge|node)\.sh|go run ./apps/(edge|node)'
(빈 매칭 — orphan edge/node 프로세스 없음. ps 출력에 보이는 bash 프로세스들은 본 검증 명령을 실행 중인 부모 shell이며 ./bin/edge.sh 나 ./bin/node.sh 패턴에는 매칭되지 않는다.)
```

각 run 모두 negative error check 통과, 4개 positive `rg`가 각각 매칭 line을 출력, session/week reset time이 서로 다름이 확인됐다 (`10am (Asia/Seoul)` vs `May 16, 6pm (Asia/Seoul)`) — G07에서 발생했던 session에 week reset이 붙는 cross-section 버그가 해소됐다.

## 코드리뷰 결과

종합 판정: FAIL

차원별 평가:

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

발견된 문제:

- Required `apps/node/internal/adapters/cli/status/claude.go:121`, `apps/node/internal/adapters/cli/status/claude.go:162`: 실제 2회 연속 smoke가 재현되지 않는다. 리뷰 재실행에서 run 1은 성공했지만 run 2(`/tmp/iop-claude-status.reviewG08.run2.wNRd6k`)는 `failed waiting for usage output: timeout waiting for complete Claude usage block`로 실패했다. failure tail에는 `Last 24h`, `d to day · w to week`, `Extra usage`, `Esc to cancel`와 함께 깨진/겹친 텍스트(`No hing ov r`, `21% of y ur usage...`)가 남았다. 이는 현재 `d`/`w` nudge와 60s wait만으로는 live Claude TUI의 repaint/cursor stream을 안정적으로 `Current session`/`Current week` block으로 복원하지 못한다는 뜻이다. raw append buffer에 regex를 적용하는 대신 cursor movement/erase를 반영한 visible-screen normalization 또는 동등한 screen-state parser를 도입해야 한다.
- Required `apps/node/internal/adapters/cli/status/parser.go:58`, `apps/node/internal/adapters/cli/status/parser.go:70`: section-aware parser가 여전히 line-oriented cleaned raw에 의존한다. `cleanANSI`가 cursor positioning/clear screen을 해석하지 않고 제거하기 때문에 실제 TUI repaint에서는 단어와 payload가 겹치거나 깨진 형태로 남고, parser가 필수 payload를 놓친다. `ParseStatusOutput`은 `cleanANSI(raw)`뿐 아니라 terminal screen을 렌더링한 visible text를 함께 파싱해야 하며, 실패 시 어떤 입력(raw-clean vs visible-screen)에서 무엇을 찾았는지 리뷰 가능한 error/tail을 제공해야 한다.
- Required `apps/node/internal/adapters/cli/status/claude_test.go:209`, `apps/node/internal/adapters/cli/status/parser_test.go:150`: 새 tests가 이번 실제 실패 형태를 아직 재현하지 못한다. `TestClaudeCheckerNavigatesUsageDayWeekViews`는 `d` 또는 `w`를 받으면 순차 line output으로 payload를 emit하는 fake라서, cursor repaint/overwrite로 텍스트가 깨지고 raw order가 visible screen order와 달라지는 실패를 잡지 못한다. 현재 run 2 failure tail에서 보인 overwrite/cursor movement fixture를 추가해 screen-aware parser 없이는 실패하고 수정 후 통과하도록 만들어야 한다.
- Required `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G08.md:138`: 검증 결과의 2회 smoke 성공 기록을 신뢰할 수 없다. 같은 최종 검증을 현재 워크트리에서 재실행하자 두 번째 run이 non-zero로 종료됐고 negative error check가 `node reported error`를 매칭했다. 최종 검증 기록은 단발 성공 로그를 붙이는 수준이 아니라, 리뷰 재실행에서 같은 명령이 통과할 만큼 안정적인 조건이어야 한다.

다음 단계: FAIL - 위 Required 항목을 해결하는 새 routed plan/review 파일을 작성한다.
