# Claude Usage Status Visible Screen Recovery Plan ## 이 파일을 읽는 구현 에이전트에게 **구현 완료 후 `CODE_REVIEW-cloud-G09.md`의 모든 섹션을 채우는 것이 필수 최종 단계입니다. 이 파일을 채우기 전까지 작업은 완료가 아닙니다.** 아래 체크리스트와 중간/최종 검증을 실제로 실행하고, 구현 내용과 명령 출력을 `CODE_REVIEW-cloud-G09.md`에 기록하세요. `CODE_REVIEW-cloud-G09.md`의 아카이브 지시는 리뷰 에이전트가 수행합니다. ## 배경 Cloud-G08은 unit test와 기록된 2회 smoke를 통과했다고 보고했지만, 리뷰 재실행에서 같은 smoke의 두 번째 run이 `timeout waiting for complete Claude usage block`로 실패했다. 실패 tail에는 `/usage` 패널 하단의 `Esc to cancel`, `d to day · w to week`, `Extra usage`와 함께 `No hing ov r`, `21% of y ur usage...`처럼 cursor repaint/overwrite가 반영되지 않은 깨진 텍스트가 남았다. 따라서 이번 라운드는 parser regex 또는 `d`/`w` nudge를 조금 고치는 수준이 아니라, PTY raw stream을 terminal visible screen으로 복원한 뒤 그 화면을 파싱하는 방향으로 수정한다. terminal-agent/TUI/PTY screen repaint 판단이 핵심이고, G08 cloud route에서도 verification trust가 깨졌으므로 build lane과 review lane 모두 `cloud-G09`로 올린다. ## 분석 결과 ### 읽은 파일 - `agent-ops/skills/common/code-review/SKILL.md` - `agent-task/cli_claude_usage_status/plan_cloud_G08_4.log` - `agent-task/cli_claude_usage_status/code_review_cloud_G08_4.log` - `apps/node/internal/adapters/cli/status/claude.go` - `apps/node/internal/adapters/cli/status/parser.go` - `apps/node/internal/adapters/cli/status/parser_test.go` - `apps/node/internal/adapters/cli/status/claude_test.go` - `apps/edge/cmd/edge/console.go` - `configs/edge.yaml` ### 실패 원인 가설 - `ParseStatusOutput`은 section-aware로 바뀌었지만 입력은 여전히 `cleanANSI(raw)` 중심이다. - `cleanANSI`는 cursor position, erase line/screen, cursor movement를 화면 상태로 적용하지 않고 대부분 제거한다. - Claude `/usage`는 live TUI라서 같은 좌표에 여러 번 repaint할 수 있다. raw append buffer에는 겹쳐 쓰기 전후 텍스트가 모두 남으므로 line regex가 실제 visible text와 다른 문자열을 보게 된다. - G08 test fake는 `d`/`w` 이후 순차 line payload를 emit한다. 실제 실패처럼 cursor-addressed repaint로 깨진 raw를 만들지 않으므로 regression을 잡지 못한다. ### 라우팅 - build lane: `cloud-G09` - review lane: `cloud-G09` - 근거: 반복된 real bin smoke 실패, interactive TUI/PTY cursor stream 의존, verification trust Fail, terminal benchmark 성격의 장기 프로세스/출력 파싱 문제. ## 의존 관계 및 구현 순서 1. `REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-1`에서 terminal visible-screen normalizer를 만든다. 2. `REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-2`에서 Claude parser/checker가 raw-clean과 visible-screen을 함께 사용하도록 연결한다. 3. `REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-3`에서 실제 실패 형태를 재현하는 tests를 추가한다. 4. `REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-4`에서 검증 신뢰를 3회 연속 smoke로 회복한다. ### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-1] Terminal visible-screen normalizer 도입 #### 문제 `apps/node/internal/adapters/cli/status/parser.go`의 `cleanANSI`는 cursor movement와 erase sequence를 해석하지 않는다. 그 결과 raw stream에 남은 중간 repaint 문자열이 파서 입력으로 들어가고, 실제 Claude 화면에 보이는 `Current session`/`Current week` payload와 다른 텍스트를 파싱하게 된다. #### 해결 방법 - `parser.go` 또는 같은 package의 기존 구조에 맞는 파일에 terminal screen normalizer를 구현한다. - 최소 지원 범위: - OSC 제거 - SGR 무시 - CSI `H`/`f` cursor position - CSI `A`/`B`/`C`/`D` cursor movement - CSI `G` cursor column - CSI `J` clear screen - CSI `K` clear line - `\r`, `\n`, backspace 처리 - printable rune을 현재 cursor 위치에 기록 - 화면 크기는 Claude status parsing에 충분한 고정 크기(예: 40x160) 또는 안전한 동적 확장으로 구현한다. - output은 trailing spaces를 정리하되 line order는 visible screen order를 보존한다. - 기존 Codex parser의 단순 ANSI/CR 처리 회귀가 없어야 한다. #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/status/parser.go` 또는 같은 package 파일 - visible-screen normalizer 구현 - [ ] cursor movement/erase를 제거하지 않고 화면 상태에 적용 - [ ] line-oriented `cleanANSI`는 필요한 경우 fallback으로 유지 - [ ] 실제 account 값, raw dump, debug file write를 repo에 남기지 않음 #### 테스트 작성 `REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-3`에서 parser fixture로 검증한다. #### 중간 검증 ```bash go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput_Claude|TestParseStatusOutput_CodexLimits' -count=1 -v ``` 기대 결과: Claude parser tests와 Codex parser regression tests가 fresh 실행으로 통과한다. ### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-2] Claude checker/parser를 visible-screen 기반으로 연결 #### 문제 `apps/node/internal/adapters/cli/status/claude.go`의 `fullUsageWait`은 raw append buffer 전체를 `ParseStatusOutput`에 넣는다. G08 smoke run2처럼 usage panel은 열렸지만 cursor repaint가 많은 경우, 필요한 payload가 화면에는 있어도 raw-clean parser가 놓칠 수 있다. #### 해결 방법 - `ParseStatusOutput`은 Claude parsing 시 아래 입력을 모두 시도한다. 1. terminal visible-screen normalized text 2. 기존 `cleanANSI(raw)` fallback - 두 입력 중 하나에서 session/week를 모두 얻으면 성공한다. - 둘 다 부분 성공이면 더 완전한 결과를 우선하고, timeout/error tail에는 raw-clean tail과 visible-screen tail 중 최소 하나를 구분 가능하게 남긴다. - `ClaudeChecker`는 "usage panel opened"와 "limits parsed"를 계속 구분한다. `Esc to cancel`/`Extra usage`만으로 성공 처리하지 않는다. - `d`/`w` nudge는 남길 수 있지만, nudge가 성공조건의 본질이 되어서는 안 된다. cursor repaint 화면 자체를 복원할 수 있어야 한다. #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/status/parser.go` - Claude parsing이 visible-screen input을 우선 또는 병행 사용 - [ ] `apps/node/internal/adapters/cli/status/claude.go` - timeout failure excerpt가 raw-clean/visible-screen 구분을 제공 - [ ] `apps/node/internal/adapters/cli/status/status.go` 변경이 필요하면 기존 status contract 유지 - [ ] Codex `5h limit`/`Weekly limit` parser 회귀 없음 - [ ] timeout 증가만으로 문제를 덮지 않음 #### 테스트 작성 `REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-3`에서 checker fake TUI로 검증한다. #### 중간 검증 ```bash go test ./apps/node/internal/adapters/cli/status/... -run 'TestClaudeChecker|TestParseStatusOutput_Claude|TestParseStatusOutput_CodexLimits' -count=1 -v ``` 기대 결과: checker/parser tests와 Codex regression이 fresh 실행으로 통과한다. ### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-3] Cursor repaint failure regression tests 추가 #### 문제 G08의 `TestClaudeCheckerNavigatesUsageDayWeekViews`는 fake TUI가 `d` 또는 `w`를 받은 뒤 순차 line output으로 payload를 emit한다. 실제 실패는 cursor-addressed repaint/overwrite 때문에 raw text가 깨지는 형태였으므로 이 test는 핵심 실패를 재현하지 못한다. #### 해결 방법 - parser test에 cursor-addressed repaint fixture를 추가한다. - raw-clean만 보면 `No hing ov r` 또는 payload split처럼 깨진 문자열이 남도록 만든다. - visible-screen normalization 후에는 `Current session`, `Current week`, `% used`, `Resets ...`가 정상 line으로 보이게 만든다. - checker test fake는 `/usage` 이후 simple sequential payload를 바로 쓰지 않는다. - `Esc to cancel`, `d to day · w to week`, `Extra usage` panel을 먼저 그린다. - cursor movement/erase로 같은 위치를 repaint해 session/week payload를 그린다. - screen-aware parser 없이는 실패해야 한다. - assertions는 `DailyLimit`, `DailyResetTime`, `WeeklyLimit`, `WeeklyResetTime`, metadata label을 모두 확인한다. #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - cursor repaint raw fixture 추가 - [ ] `apps/node/internal/adapters/cli/status/claude_test.go` - cursor-addressed fake TUI 추가 또는 기존 test 강화 - [ ] 기존 happy-path tests 유지 - [ ] test fixture에는 synthetic reset time만 사용 - [ ] test가 단순 sleep/timeout 증가만으로 통과하지 않게 구성 #### 테스트 작성 필수 test name: - `TestParseStatusOutput_ClaudeUsageFromVisibleScreenRepaint` - `TestClaudeCheckerParsesCursorRepaintedUsageScreen` #### 중간 검증 ```bash go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput_ClaudeUsageFromVisibleScreenRepaint|TestClaudeCheckerParsesCursorRepaintedUsageScreen|TestParseStatusOutput_CodexLimits' -count=1 -v ``` 기대 결과: cursor repaint regression tests와 Codex parser test가 fresh 실행으로 통과한다. ### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-4] 검증 신뢰 회복 및 3회 연속 smoke #### 문제 G08은 `CODE_REVIEW-cloud-G08.md`에 2회 smoke 성공을 기록했지만, 리뷰 재실행에서 동일 계열 smoke의 두 번째 run이 실패했다. 따라서 다음 구현은 단발 또는 우연한 2회 성공을 완료로 기록하면 안 된다. #### 해결 방법 - 최종 smoke는 3회 연속 실행한다. - 각 run에서 아래 조건을 모두 만족해야 한다. - `node reported error`, `timeout waiting`, `raw output did not include parsed limits`가 없어야 한다. - `[edge] sent command=status ... target=claude`가 있어야 한다. - `[node-...-status] target=claude`가 있어야 한다. - `Current session` 또는 `Daily limit` remaining/reset line이 있어야 한다. - `Current week` 또는 `Weekly limit` remaining/reset line이 있어야 한다. - failure excerpt에는 raw-clean과 visible-screen 중 무엇이 실패했는지 판단 가능한 tail을 남긴다. - smoke 뒤에는 해당 run에서 띄운 `edge_pid`/`node_pid`가 종료됐는지 확인한다. 오래된 shell history나 다른 세션의 unrelated command line을 orphan으로 오판하지 않는다. #### 수정 파일 및 체크리스트 - [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G09.md` - 3회 smoke stdout/stderr 기록 - [ ] 각 run의 tmpdir, negative check, positive `rg` checks 기록 - [ ] run별 `edge_pid`/`node_pid` 종료 확인 기록 - [ ] `find`로 repo 내부 probe/raw/diag artifact 없음 확인 #### 테스트 작성 별도 Go test 없음. #### 중간 검증 ```bash find . -maxdepth 3 \( -name '*diag*' -o -name '*raw*' -o -name '*probe*' \) -not -path './.git/*' -print ``` 기대 결과: repo 안에 진단 artifact가 없다. ## 수정 파일 요약 | 파일 | 항목 | |------|------| | `apps/node/internal/adapters/cli/status/parser.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-1, REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-2 | | `apps/node/internal/adapters/cli/status/claude.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-2 | | `apps/node/internal/adapters/cli/status/parser_test.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-3 | | `apps/node/internal/adapters/cli/status/claude_test.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-3 | | `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G09.md` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API-4 | ## 최종 검증 ```bash go test ./apps/node/internal/adapters/cli/status/... -count=1 go test ./apps/edge/cmd/edge -count=1 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 git diff --check command -v claude ``` 기대 결과: Go tests가 fresh 실행으로 통과하고, `gofmt -l` 출력이 비어 있으며, `git diff --check`가 성공하고, Claude binary path가 출력된다. ```bash set -euo pipefail for run in 1 2 3; do tmpdir="$(mktemp -d /tmp/iop-claude-status.G09.run${run}.XXXXXX)" echo "run=$run tmpdir=$tmpdir" mkfifo "$tmpdir/edge.in" ./bin/edge.sh < "$tmpdir/edge.in" > "$tmpdir/edge.out" 2>&1 & edge_pid=$! node_pid="" cleanup_run() { kill "${node_pid:-}" "$edge_pid" 2>/dev/null || true exec 9>&- 2>/dev/null || true } trap cleanup_run EXIT 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 180 bash -c 'while kill -0 "$0" 2>/dev/null; do sleep 1; done' "$edge_pid" kill "$node_pid" "$edge_pid" 2>/dev/null || true exec 9>&- trap - EXIT echo "--- EDGE.OUT run=$run ---" sed -n '1,280p' "$tmpdir/edge.out" echo "--- NODE.OUT run=$run ---" sed -n '1,240p' "$tmpdir/node.out" echo "--- NEGATIVE run=$run ---" ! rg --sort path -n 'node reported error|timeout waiting|raw output did not include parsed limits' "$tmpdir/edge.out" echo "--- EDGE STATUS run=$run ---" rg --sort path -n '\[edge\] sent command=status .*target=claude' "$tmpdir/edge.out" echo "--- NODE STATUS run=$run ---" rg --sort path -n '\[node-.*-status\] target=claude' "$tmpdir/edge.out" echo "--- SESSION/DAILY run=$run ---" rg --sort path -n 'Current session: [0-9]+(\.[0-9]+)?% remaining \(resets .+\)|Daily limit: [0-9]+(\.[0-9]+)?% remaining \(resets .+\)' "$tmpdir/edge.out" echo "--- WEEK/WEEKLY run=$run ---" rg --sort path -n 'Current week: [0-9]+(\.[0-9]+)?% remaining \(resets .+\)|Weekly limit: [0-9]+(\.[0-9]+)?% remaining \(resets .+\)' "$tmpdir/edge.out" echo "--- PID CLEANUP run=$run ---" ! kill -0 "$edge_pid" 2>/dev/null ! kill -0 "$node_pid" 2>/dev/null done ``` 기대 결과: 세 run 모두 command가 non-zero 없이 완료되고, negative check는 매칭이 없으며, positive `rg` 4개가 각각 matching line을 출력한다. 하나라도 실패하면 작업은 미완료다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-cloud-G09.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.