<!-- task=m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping plan=1 tag=REVIEW_REFACTOR -->

# Code Review Reference - REVIEW_REFACTOR

> **[IMPLEMENTING AGENT — READ FIRST]** 구현 에이전트는 plan/review 모델과 달라야 한다. 구현 소유 섹션을 채우고 `review-ready`를 보고한 뒤 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

## 개요

date=2026-07-21
task=m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping, plan=1, tag=REVIEW_REFACTOR

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md)
- Task ids:
  - `offline-snapshot`: configured offline Node/provider와 reconnect snapshot 수렴
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- 이전 pair: `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/plan_local_G06_0.log`, `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/code_review_local_G06_0.log`
- 이전 FAIL Required: 전체 Go 회귀가 CLI adapter의 one-shot cancellation 및 persistent terminal 다섯 시나리오에서 실패했다. connector mapping/wire/race evidence는 PASS였다.

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

> **[REVIEW AGENT ONLY]** 구현 에이전트와 다른 모델이 실제 diff와 검증 출력을 대조한다. product source/test를 수정하거나 plan을 구현하지 않는다.

1. 판정을 append한다.
2. `CODE_REVIEW-local-G08.md`를 `code_review_local_G08_1.log`, `PLAN-local-G08.md`를 `plan_local_G08_1.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 task를 archive한다. WARN/FAIL이면 user-review gate 또는 다른 Plan 모델의 follow-up pair만 만든다.
4. PASS이면 runtime용 Milestone 완료 이벤트를 보고하며 roadmap을 직접 수정하지 않는다.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|---|---|
| REVIEW_REFACTOR-1 One-shot cancellation 반환 경로 고정 | [x] |
| REVIEW_REFACTOR-2 Persistent PTY lifecycle fixture 안정화 | [x] |

## 구현 체크리스트

- [x] one-shot stdout-read/command-wait 경쟁에서 context cancellation이면 `ErrRunCancelled`와 `user-cancel`/`timeout` event를 한 번만 반환하도록 수정하고 회귀 테스트를 추가한다.
- [x] persistent terminal startup, carriage-return, Claude session-limit cancellation, startup-ready replay, chrome filtering의 관찰 순서를 안정화하고 기존 사용자-visible 입력·출력 계약을 보존하는 테스트를 갱신한다.
- [x] 반복 package/race, Node/전체 Go 회귀와 mock smoke를 실행하고, full-cycle 및 외부 CLI profile은 성공 또는 명시 blocker를 실제 출력과 함께 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채우고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 PASS, WARN, FAIL 중 하나를 append하고 차원별 평가와 Required/Suggested/Nit를 일치시킨다.
- [x] active review를 `code_review_local_G08_1.log`, plan을 `plan_local_G08_1.log`로 archive한다.
- [x] `.gitignore`의 Agent-Ops block이 task Markdown/log를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `complete.log`에 `Roadmap Completion: offline-snapshot`과 prior connector mapping/wire evidence, CLI package/race/Node/전체 Go 회귀 evidence를 기록하고 active `.md`를 남기지 않는다.
- [ ] PASS이면 task directory를 월별 archive로 이동하고 runtime Milestone 완료 이벤트를 보고하되 roadmap/update-roadmap은 직접 호출하지 않는다.
- [ ] PASS split 이동 후 active parent에 sibling/file이 남아 있으면 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 아니면 pair만 archive해 `plan-ready`로 전환한다. review 모델은 직접 구현하지 않는다.
- [ ] USER_REVIEW이면 template으로 `USER_REVIEW.md`를 만들고 active pair와 `complete.log`를 남기지 않는다.

## 계획 대비 변경 사항

계획의 최소 diff에 더해, Required 검증(`-race ./apps/node/internal/adapters/cli`, `-count=1 ./...`)이 실제로 통과하도록 같은 근본 원인을 공유하는 인접 결함을 함께 고쳤다. 계획 `수정 파일 요약` 표 밖 파일 변경은 여기에 명시한다.

- **REVIEW_REFACTOR-1 (oneshot.go, 계획 범위 + 확장)**: 계획의 read-error→cancel 반환 경로를 그대로 넣었고, 추가로 (a) `cmd.Start()`가 ctx done으로 실패하는 경우와 (b) ctx.Done 시 stdout/stderr pipe를 닫아 pending read를 즉시 unblock하는 watcher를 넣었다. 이유: read-error 경로만으로는 -race/부하에서 나타나는 pre-start 취소와, `CommandContext`가 직접 자식만 죽여 forked grandchild가 pipe를 잡고 있을 때 read가 최대 10초 정지하는 문제를 못 막았다. 두 경우 모두 "단일 cancelled event + 신속 반환" 계약을 깼다.
- **oneshot user-cancel 테스트 재설계**: 고정 100ms sleep 후 취소 대신, 프로세스의 첫 stdout chunk(`echo running`)를 관측한 뒤에만 취소하도록 바꿔 pre-start 취소 경쟁을 제거했다. cancelled event가 정확히 하나·error event 없음을 반복 검증한다.
- **REVIEW_REFACTOR-2**: persistent.go의 terminal sequencing 자체에는 product 결함이 없었다(실제 CLI는 startup 화면을 스트리밍하므로 idle-drain이 자연 동기화되지만, 테스트 helper는 startup 출력이 없어 drain이 준비 전에 종료됐다). 수정은 대부분 fixture 쪽 handshake다. 단, persistent.go에는 계획이 허용한 "실제 defect 최소 수정" 1건을 넣었다: `run.setup()`의 writePrompt 중 ctx가 취소되면 ERROR가 아니라 단일 cancelled event + `ErrRunCancelled`를 반환한다(-race 전체 패키지 부하에서 노출).
- **chrome 결정화(계획의 timer 지양 준수)**: 이 환경의 프로세스 spawn 지연이 41~725ms로 극심하게 변동해 fixed timer로 `stty -echo` 경쟁을 결정적으로 덮을 수 없었다. 프레임 앞에 cursor-home+erase-display(`\033[H\033[2J`, 실제 claude TUI redraw와 동일)를 넣어, 에코된 prompt가 assistant 라인 렌더 전에 지워지도록 하여 echo 경쟁에 무관하게 결정화했다.
- **session-limit / replay helper 결정화**: session-limit helper가 `\r` 또는 `\n`에 반응하도록(line-discipline 변환에 무관), replay helper는 trust-screen auto-accept 의존과 fixed 150ms sleep을 제거하고 첫 prompt를 버린 뒤 ready 화면 후 replay를 blocking-read로 동기화하도록 재설계했다.
- **5개 시나리오 밖(계획 파일 표 밖) 변경 — Required 검증을 green으로 만들기 위해 같은 근본 원인 인접 결함 수정**:
  - `opencode_sse.go`: prompt-async POST가 취소 중 "context canceled"를 반환하면 `ErrRunCancelled`로 반환(REVIEW_REFACTOR-1과 동일 클래스). -race 전체 패키지에서 노출된 pre-existing flake.
  - `persistent_terminal_test.go`의 bypass-warning/trust-warning: 동일 startup-race라 `StartupIdleTimeoutMS` 250→800.
  - 완료-대기형 sibling 테스트(`persistent_completion_test.go` ×4, `persistent_process_test.go`, 그리고 carriage-return 자신)의 execCtx 1s→5s. carriage-return은 `StartupIdleTimeoutMS`가 800ms가 되면서 1s execCtx로는 완료 전에 만료되어 필수였다. 이 값들은 검증 대상이 아니라 anti-hang 안전 상한이다.

## 주요 설계 결정

- **취소 계약 통일**: one-shot(read-error·start-error), persistent(setup writePrompt), opencode(prompt-async POST) 모두, 연산 실패 시점에 ctx가 done이면 `cancelEventForContext`(user-cancel/timeout) 메시지의 cancelled event를 background context로 **정확히 한 번** emit하고 `runtime.ErrRunCancelled`를 반환한다. 기존 wait-branch 취소 의미는 그대로 보존한다.
- **one-shot pipe-close watcher**: ctx.Done 시 stdout/stderr를 닫아 pending read를 즉시 종료한다. `exec.CommandContext`는 직접 자식만 signal하므로 orphan grandchild가 pipe를 잡으면 read가 정지한다. `Wait`가 어차피 닫는 pipe를 미리 닫는 것은 안전하다(Wait는 close 오류 무시).
- **production terminal 계약 보존**: character pacing + `\r` terminator(`writePrompt`/`terminal.WritePrompt`), idle 기반 startup drain, ready 후 prompt replay, tail 기반 cancel/ready 판정, chrome 필터링은 모두 불변이다. product 코드 변경은 persistent setup 취소 반환 경로 1건뿐이다.
- **테스트 fixture를 실제 CLI에 맞춤**: 실제 CLI는 startup 출력을 스트리밍해 idle-drain이 자연 동기화되지만 최소 helper는 아무 것도 안 내보냈다. 그래서 raw-mode/`-echo` 설정 **뒤에만** ready 마커를 내보내 drain을 실제 준비 지점에 anchor시켰고, 마커는 startup drain에서 소비되어 delta로 새지 않는다. 이 환경의 변동 spawn 지연 때문에 마커로도 결정화가 어려운 경우(chrome echo, session-limit terminator)는 fixture를 spawn-timing 무관하게 만들었다(clear-screen redraw, `\r|\n` 수용).
- **환경 제약(측정)**: re-exec 테스트 바이너리 cold start와 `sh`+`stty` spawn이 격리 실행에서 41~725ms로 변동한다. startup 마커를 쓰는 시나리오의 `StartupIdleTimeoutMS`는 이 spawn 상한을 넘도록 크게 잡았다(carriage-return/bypass/trust=800ms). 이는 timer 확대 단독 해결이 아니라 handshake 마커를 성립시키기 위한 사이징이며, 잔여 위험은 아래 검증 결과에 기록했다.

## 사용자 리뷰 요청

_선택 Milestone `구현 잠금 > 결정 필요`가 구현을 막을 때만 채운다. 다른 환경/검증 blocker는 검증 결과에 기록한다._

- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 후속으로 해결할 수 없는 이유: 없음
- 재개 조건: 없음

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

- one-shot context cancellation이 stdout pipe close 경로에서도 `ErrRunCancelled` 및 정확히 하나의 cancellation event로 수렴하는가.
- raw terminal의 `\r` 입력, session-limit cancellation, startup-ready replay, terminal chrome filtering이 fixed sleep에 의존하지 않고 기존 event/delta/completion 계약을 보존하는가.
- targeted repeat 20회, race, Node package, 전체 Go 회귀가 모두 종료 코드 0인가.
- prior connector mapping의 true/false connected, provider tuple, config non-leak wire evidence와 S14 범위를 훼손하지 않았는가.
- full-cycle/외부 CLI profile 미실행은 실제 blocker와 남은 위험으로 남았는가.

## 검증 결과

### gofmt

`gofmt -d apps/node/internal/adapters/cli/oneshot.go apps/node/internal/adapters/cli/oneshot_blackbox_test.go apps/node/internal/adapters/cli/opencode_sse.go apps/node/internal/adapters/cli/persistent.go apps/node/internal/adapters/cli/persistent_completion_test.go apps/node/internal/adapters/cli/persistent_process_test.go apps/node/internal/adapters/cli/persistent_terminal_test.go apps/node/internal/adapters/cli/persistent_test_support_test.go`

```text
(출력 없음 — 변경한 8개 Go 파일 모두 gofmt clean)

변경 파일 diffstat (git diff --stat apps/node/internal/adapters/cli/):
 oneshot.go                     | 40 ++++++++++
 oneshot_blackbox_test.go       | 53 +++++++++++------
 opencode_sse.go                | 12 +++
 persistent.go                  | 12 +++
 persistent_completion_test.go  |  8 +--
 persistent_process_test.go     |  2 +-
 persistent_terminal_test.go    | 39 +++++++++---
 persistent_test_support_test.go| 32 +++++++---
 8 files changed, 159 insertions(+), 39 deletions(-)
```

### CLI focused repeat

`go test -count=20 ./apps/node/internal/adapters/cli -run '^(TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage|TestCLIExecutePersistentTerminalSendsCarriageReturn|TestCLIExecutePersistentClaudeTUICancelsSessionLimitUpgradePrompt|TestCLIExecutePersistentClaudeTUIReplaysPromptAfterStartupReadyRace|TestCLIExecutePersistentClaudeTUIFiltersTerminalChrome)$'`

```text
ok  	iop/apps/node/internal/adapters/cli	65.379s
(5개 시나리오 × 20회 모두 PASS. 수정 전 동일 명령은 carriage-return/session-limit/replay/chrome에서 결정적/간헐 실패했다.)
```

### race 및 회귀

`go test -race -count=1 ./apps/node/internal/adapters/cli`

`go test -count=1 ./apps/node/...`

`go test -count=1 ./...`

```text
# race (전체 cli 패키지, count=1)
ok  	iop/apps/node/internal/adapters/cli	52.386s
# race 안정성 확인용 count=2 도 PASS (103.5s)

# node 전체 패키지
ok  	iop/apps/node/internal/adapters/cli	46.067s
ok  	iop/apps/node/internal/adapters/cli/status	45.939s
ok  	iop/apps/node/internal/{bootstrap,node,router,store,terminal,transport,...}	PASS
(FAIL package 없음)

# 전체 Go 회귀
$ go test -count=1 ./...  → exit 0
FAIL lines: 0, ok packages: 34
(수정 후 ./... 를 6회 반복 실행: 최초 execCtx 상향 전 1회 idle-reason(내 5개 범위 밖 sibling) flake, execCtx 1s→5s 상향 후 5/5 PASS. race 전체 패키지에서도 pre-start 취소·opencode·persistent setup 취소 flake를 각각 수정하며 순차적으로 green화했다.)
```

참고(잔여 위험): 이 환경의 프로세스 spawn 지연이 41~725ms로 크게 변동한다. terminal fixture는 handshake 마커 + 넉넉한 `StartupIdleTimeoutMS`(800ms) 및 완료-대기 execCtx(5s)로 이 상한을 덮도록 사이징했고 반복 검증으로 green을 확인했으나, 극단적 부하에서의 spawn 스파이크는 환경 요인으로 남는다. chrome/session-limit/replay/oneshot은 spawn-timing 무관하게 결정화되어 이 위험에서 자유롭다.

### smoke/full-cycle/profile

`IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh`

`scripts/dev/edge.sh` + `scripts/dev/node.sh` (임시 config의 mock CLI full-cycle)

```text
# mock e2e smoke — PASS (exit 0)
[node-event] start run_id=manual-...        # fake-cli persistent run
[node-message] IOP_E2E_READY_SHORT
[node-event] complete run_id=manual-... detail="idle-timeout"
... (READY/BYE/YES 3회 run 모두 complete=idle-timeout)
[node] SESSION_LIST → TERMINATE_SESSION(CANCEL_ACTION_TERMINATE_SESSION) → disconnected
[e2e] Auxiliary smoke test PASSED.
→ 실제 Edge+Node+CLI adapter 경로에서 persistent run 완료(idle-timeout)와 session terminate/cancel까지 검증됨.

# scripts/dev/edge.sh + scripts/dev/node.sh full-cycle — BLOCKER (환경)
edge.sh = `iop-edge console` (대화형 REPL, foreground exec),
node.sh = `iop-node serve` (foreground, edge 연결 대기).
두 개의 대화형/long-running 프로세스를 별도 터미널에서 띄우고 run/cancel/session/status/terminate를
수동 입력해야 하므로 비대화형 세션에서 실행 불가. mock e2e-smoke가 동등한 자동 full-cycle
(실제 Edge+Node 연결, 등록, run 완료, session list, terminate)을 이미 커버함.
재개 조건: 대화형 2-프로세스 터미널 환경.

# 외부 CLI profile (claude/codex/antigravity/opencode 등 실제 provider) — BLOCKER (계정/설치)
실제 provider 바이너리 설치·계정 인증이 필요해 이 환경에서 실행 불가.
CLI adapter 실행/취소/터미널 계약은 mock/fake-cli profile과 PTY helper(raw-tui, claude-tui 시나리오)로 검증함.
재개 조건: 대상 provider 바이너리 설치 + 계정 자격.
```

---

> **[IMPLEMENTING AGENT — BEFORE SAVING]** completion table, implementation checklist, 계획 대비 변경 사항, 주요 설계 결정, 검증 stdout/stderr를 모두 채운 뒤 `review-ready`를 보고하고 종료한다. review-only sections는 수정하지 않는다.

## 섹션 소유권

| Section | Owner | Note |
|---|---|---|
| Header, 개요, Roadmap Targets, Archive Evidence Snapshot, 리뷰 지시 | Fixed | 구현 에이전트 수정 금지 |
| 구현 표·체크리스트·계획 대비 변경 사항·설계 결정·검증 출력 | Implementing agent | 실제 내용 필수 |
| 코드리뷰 전용 체크리스트·코드리뷰 결과 | Review agent | 구현 에이전트 수정 금지 |
| 사용자 리뷰 요청 | Implementing agent | Milestone 잠금 결정만 사용 |

## 코드리뷰 결과

- 종합 판정: FAIL
- 리뷰 재검증:
  - `gofmt -d` (변경한 CLI Go 파일 8개) 및 `git diff --check -- apps/node/internal/adapters/cli apps/edge/internal/controlplane` — PASS, 출력 없음.
  - `go test -count=20 ./apps/node/internal/adapters/cli -run '^(TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage|TestCLIExecutePersistentTerminalSendsCarriageReturn|TestCLIExecutePersistentClaudeTUICancelsSessionLimitUpgradePrompt|TestCLIExecutePersistentClaudeTUIReplaysPromptAfterStartupReadyRace|TestCLIExecutePersistentClaudeTUIFiltersTerminalChrome)$'` — PASS (20회 반복).
  - `go test -race -count=1 ./apps/node/internal/adapters/cli`, `go test -count=1 ./apps/node/...`, `go test -count=1 ./...` — PASS.
  - `IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh` — PASS. `scripts/dev/edge.sh` + `scripts/dev/node.sh`의 대화형 full-cycle 및 실제 외부 CLI profile은 구현 기록의 환경 blocker가 유지된다.
- 차원별 평가:
  - Correctness: Fail — `prompt_async` POST 중 취소된 Opencode 요청이 서버 세션을 abort하지 않아, 서버가 이미 요청을 수락한 경우 작업이 계속 실행될 수 있다.
  - Completeness: Fail — 계획 밖으로 추가된 Opencode 취소 경로가 기존 cancel lifecycle(세션 abort)을 보존하지 못했다.
  - Test coverage: Fail — 새 `prompt_async`-in-flight 취소 분기는 응답을 지연한 뒤 abort·단일 cancelled event를 검증하는 결정적 회귀 테스트가 없다.
  - API contract: Pass — Edge-Node wire 및 기존 Control Plane connected mapping의 public 계약을 바꾸지 않았다.
  - Code quality: Fail — 기존 `opencodeSSERun.finalize`의 취소 정리를 우회해 취소 lifecycle이 두 경로에서 불일치한다.
  - Implementation deviation: Warn — 전체 회귀를 위한 인접 수정으로 `opencode_sse.go`를 추가 변경했으나, 그 변경이 기존 취소 보장을 일부 누락했다.
  - Verification trust: Pass — 현재 worktree에서 반복, race, Node 전체, 전체 Go 회귀 및 mock smoke가 실제 종료 코드 0으로 재현됐다.
  - Spec conformance: Pass — S14 `offline-snapshot`의 prior Control Plane snapshot mapping/wire 근거를 훼손하지 않았다.
- 발견된 문제:
  - Required — `apps/node/internal/adapters/cli/opencode_sse.go:168`: `opencodePromptAsync`가 context 취소로 반환하면 새 분기는 cancelled event와 `ErrRunCancelled`만 반환한다. 서버가 POST를 이미 수락하고 응답 전송이 지연된 경우 기존 `driveOpencodeSSE` 취소 경로와 달리 `opencodeAbort`가 호출되지 않아 취소된 prompt가 세션에서 계속 실행될 수 있다. background timeout context로 동일 session의 abort를 수행하고, prompt 수락 뒤 응답을 block한 fixture에서 abort 호출·정확히 한 cancelled event·`ErrRunCancelled`을 검증해야 한다.
- 다음 단계: FAIL 후속 계획 — Opencode prompt-async 취소 시 server abort lifecycle과 결정적 회귀 테스트를 추가한 뒤 다시 검토한다. 사용자 리뷰 게이트는 해당하지 않는다.
