<!-- task=cli_real_e2e_failures/01_claude_tui plan=1 tag=REVIEW_CTUI -->

# PLAN-cloud-G07 - REVIEW_CTUI persistent exit diagnostic follow-up

## 이 파일을 읽는 구현 에이전트에게

이 계획은 직전 코드리뷰에서 발견된 Required 이슈만 고친다. 구현 후 검증 명령을 실제로 실행하고, `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션에 실제 구현 내용과 stdout/stderr를 붙여 넣은 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. `코드리뷰 결과`, 로그 rename, archive, `complete.log` 작성은 코드리뷰 에이전트 전용이다.

## 배경

직전 구현은 `claude-tui` 종료 진단 tail을 추가했지만, persistent process 종료 시 `sess.output` close branch가 먼저 선택되면 `sess.done`의 exit status를 읽지 않는다. 그래서 새 회귀 테스트가 요구하는 `exit status 2`가 실제 재실행에서 누락된다. 이 후속 작업은 exit diagnostic 경로를 하나로 모아 테스트를 안정화한다.

## 분석 결과

### 읽은 파일

- `agent-task/cli_real_e2e_failures/01_claude_tui/plan_cloud_G07_0.log`
- `agent-task/cli_real_e2e_failures/01_claude_tui/code_review_cloud_G07_0.log`
- `apps/node/internal/adapters/cli/cli.go`
- `apps/node/internal/adapters/cli/persistent.go`
- `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go`
- `apps/node/internal/adapters/cli/internal/testutil/testutil.go`
- `configs/edge.yaml`
- `agent-task/cli_real_e2e_failures/01_claude_tui/edge-tui.yaml`

### 테스트 커버리지 공백

- `TestCLIExecutePersistentProcessExitReturnsError`는 tail과 exit status를 확인하지만 현재 구현에서는 `sess.output` close branch가 먼저 선택될 때 실패한다.
- `go test ./apps/node/internal/adapters/cli`는 cache 출력만으로는 신뢰할 수 없었다. 후속 검증은 `-count=1` 또는 반복 실행을 사용한다.
- 실제 `claude-tui` E2E는 persistent terminal lifecycle 경로에 닿으므로 수정 후 다시 수행해야 한다.

### 심볼 참조

- renamed/removed symbol: 없음.
- `profileSession.done`은 `apps/node/internal/adapters/cli/persistent.go:211`에서 실행 중 종료를 받는다.
- `doneCh`는 `apps/node/internal/adapters/cli/persistent.go:364`, `apps/node/internal/adapters/cli/persistent.go:402`에서 `cmd.Wait()` 결과를 보낸 뒤 `outputCh`를 닫는다.
- `persistent session process exited` 진단 문자열은 `apps/node/internal/adapters/cli/persistent.go:130`, `apps/node/internal/adapters/cli/persistent.go:223`에 중복되어 있다.

### 분할 판단

분할 정책을 검토했다. Required 이슈가 하나이고 `apps/node/internal/adapters/cli/persistent.go`의 동일 실행 종료 경로와 해당 블랙박스 테스트만 다루므로 단일 plan이 적합하다. 새 API, 광범위 call-site rollout, 별도 데이터/프로토콜 경계가 없어 추가 subtask로 나누지 않는다.

### 범위 결정 근거

범위는 persistent session exit diagnostic 안정화로 제한한다. `apps/node/internal/adapters/cli/opencode_sse*.go`와 `opencode` 테스트 변경은 현재 worktree에 있어도 이 plan의 대상이 아니다. `configs/edge.yaml`은 실제 `claude-tui` E2E가 새 원인을 드러낼 때만 수정하고, 그 경우 `계획 대비 변경 사항`에 이유를 기록한다.

### 빌드 등급

- Lane: `cloud`
- Grade: `G07`
- 근거: PTY/persistent process control, stdout/stderr 진단, exit-status contract, 실제 외부 CLI E2E 검증이 필요한 terminal-agent 후속 작업이다.

## 구현 체크리스트

- [ ] `sess.output` close branch와 `sess.done` branch가 같은 persistent exit diagnostic formatter를 사용하고 exit status를 안정적으로 포함한다.
- [ ] `TestCLIExecutePersistentProcessExitReturnsError`가 fresh/repeated 실행에서 tail과 exit status를 모두 검증하며 통과한다.
- [ ] 실제 `bin/edge.sh` + `bin/node.sh` 흐름으로 `claude-tui` 메시지 2회를 재검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 구현 계획

### [REVIEW_CTUI-1] persistent exit diagnostic branch를 단일화한다

문제:

- [apps/node/internal/adapters/cli/persistent.go:120](/config/workspace/iop/apps/node/internal/adapters/cli/persistent.go:120)의 `sess.output` close branch는 session map에서 제거한 뒤 tail만 붙이고 반환한다.
- 같은 process exit에서 [apps/node/internal/adapters/cli/persistent.go:211](/config/workspace/iop/apps/node/internal/adapters/cli/persistent.go:211)의 `sess.done` branch를 타면 exit status가 포함된다. select branch에 따라 진단 품질과 테스트 결과가 달라진다.
- 실제 재현 명령:

```bash
go test ./apps/node/internal/adapters/cli -run '^TestCLIExecutePersistentProcessExitReturnsError$' -count=50
go test ./apps/node/internal/adapters/cli -count=1
```

해결 방법:

- `persistent.go`에 session 제거와 diagnostic formatting을 공유하는 helper를 둔다.
- `sess.output` close branch에서는 `sess.done`을 non-blocking으로 회수한다. 현재 reader goroutine은 `cmd.Wait()` 결과를 `doneCh`에 보낸 뒤 `outputCh`를 닫으므로 output closed 상태에서는 exit error를 읽을 수 있어야 한다.
- exit error가 없을 때만 기존 `unexpectedly` 계열 문구를 유지하고, exit error가 있으면 `exit status N`을 포함한다.

Before:

```go
case out, ok := <-sess.output:
	if !ok {
		// Process exited — remove the dead session.
		c.mu.Lock()
		if s, found := c.sessions[sess.key]; found && s == sess {
			delete(c.sessions, sess.key)
		}
		c.mu.Unlock()
		tail := sess.getTail()
		cmdSummary := fmt.Sprintf("%s %s", profile.Command, strings.Join(profile.Args, " "))
		msg := fmt.Sprintf("persistent session process exited unexpectedly: target=%s, session=%s, command=%q", targetName, sess.key.sessionID, cmdSummary)
		if tail != "" {
			msg = fmt.Sprintf("%s, recent output: %q", msg, tail)
		}
		return emitRuntimeError(ctx, sink, spec.RunID, msg)
	}
```

After:

```go
case out, ok := <-sess.output:
	if !ok {
		err := drainPersistentDone(sess)
		return c.emitPersistentExit(ctx, sink, spec.RunID, targetName, profile, sess, err)
	}
```

수정 파일 및 체크리스트:

- `apps/node/internal/adapters/cli/persistent.go`
  - [ ] session 제거, command summary, tail formatting 중복을 helper로 모은다.
  - [ ] output close branch가 `sess.done`의 exit error를 회수한다.
  - [ ] 기존 cancel/idle/completion 흐름은 변경하지 않는다.
- `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go`
  - [ ] `TestCLIExecutePersistentProcessExitReturnsError`가 `err.Error()`와 error event 모두에서 tail 및 exit status를 확인하게 유지하거나 강화한다.
  - [ ] 불필요한 임시 `t.Logf`가 남지 않게 한다.

테스트 작성:

- 새 테스트 파일은 만들지 않는다. 기존 `TestCLIExecutePersistentProcessExitReturnsError`가 이번 회귀를 직접 잡고 있으므로 해당 테스트를 안정적으로 통과시키는 쪽으로 보강한다.

중간 검증:

```bash
go test ./apps/node/internal/adapters/cli -run '^TestCLIExecutePersistentProcessExitReturnsError$' -count=50
```

예상 결과: 50회 모두 통과하고, 실패 출력에 `persistent session process exited unexpectedly`만 있고 exit status가 없는 사례가 없어야 한다.

## 수정 파일 요약

| 파일 | 항목 | 수정 의도 |
| --- | --- | --- |
| `apps/node/internal/adapters/cli/persistent.go` | REVIEW_CTUI-1 | output close/done 종료 진단 단일화 및 exit status 보존 |
| `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` | REVIEW_CTUI-1 | 종료 진단 회귀 테스트 안정화 및 실제 error event 검증 |
| `agent-task/cli_real_e2e_failures/01_claude_tui/CODE_REVIEW-cloud-G07.md` | REVIEW_CTUI-1 | 구현 결과와 실제 검증 stdout/stderr 기록 |

## 최종 검증

```bash
go test ./apps/node/internal/adapters/cli -run '^TestCLIExecutePersistentProcessExitReturnsError$' -count=50
go test ./apps/node/internal/adapters/cli -run 'Persistent|ClaudeTUI|Lifecycle' -count=1
go test ./apps/node/internal/adapters/cli -count=1
```

실제 사용자 흐름:

```bash
./bin/node.sh --config agent-task/cli_real_e2e_failures/01_claude_tui/edge-tui.yaml
./bin/edge.sh --config agent-task/cli_real_e2e_failures/01_claude_tui/edge-tui.yaml
```

edge console에서 `/nodes`, `/capabilities node0`, `/transport node0`, `IOP_E2E_CLAUDE_TUI_ONE`, `IOP_E2E_CLAUDE_TUI_TWO`, `/sessions`, `/terminate-session`을 입력한다. 두 메시지 모두 non-empty `[node0-msg]`와 `[node0-evt] complete`가 있어야 한다. 실제 stdout/stderr가 길면 repo 밖 `/tmp/...` 로그 경로와 핵심 grep 출력을 `CODE_REVIEW-cloud-G07.md`에 기록한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
