iop/agent-task/cli_claude_usage_status/plan_cloud_G09_5.log

273 lines
14 KiB
Text

<!-- task=cli_claude_usage_status plan=5 tag=REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API -->
# 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`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.