<!-- task=m-openai-workspace-agent-execution-contract/05_failure_surface plan=1 tag=REVIEW_FAILURE_SURFACE -->

# Plan - REVIEW_FAILURE_SURFACE

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

`CODE_REVIEW-local-G07.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 검증을 실행하고 실제 출력과 구현 메모를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청` 섹션에 증거와 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

첫 리뷰에서 workspace preflight helper 자체와 직접 반환 error는 추가되었지만, foreground Node 실행에서 caller가 소비하는 표면은 `adapter.Execute`의 반환값이 아니라 `RuntimeEvent{Type: error}` 스트림이다. `apps/node/internal/transport/session.go`는 `OnRunRequest` error를 로그만 남기므로, preflight/session 생성 실패가 error event 없이 반환되면 OpenAI-compatible caller는 `workspace not found` 같은 분류를 받지 못하고 timeout/stream close로 보일 수 있다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md`
- Task ids:
  - `failure-surface`: workspace 누락, 존재하지 않는 경로, 권한 오류, agent process exit failure가 호출자에게 구분 가능한 실패로 드러난다.
- Completion mode: check-on-pass

## 리뷰 근거

- Archived plan: `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/plan_local_G06_0.log`
- Archived review: `agent-task/m-openai-workspace-agent-execution-contract/05_failure_surface/code_review_local_G06_0.log`
- Required issue 1: pre-start workspace/session errors return before emitting a run error event.
- Required issue 2: regression tests assert direct returned errors but not the run-stream error event, and process-exit status is logged instead of asserted.

## 범위 결정 근거

- Edge HTTP missing/relative workspace validation은 이미 기존 테스트가 커버하므로 status/message를 바꾸지 않는다.
- Node transport protocol이나 Edge event bus 구조는 바꾸지 않는다. CLI adapter가 pre-start 실패를 runtime error event로 내보내게 만들어 기존 stream 수집 경로에 맞춘다.
- NomadCode metadata shape, authoring HTTP smoke, roadmap 문서 갱신은 이 follow-up 범위가 아니다.

## 빌드 등급

- `local-G07`: Node CLI adapter와 Edge/OpenAI caller surface 사이의 이벤트 계약을 다루지만, 수정 범위는 CLI adapter error propagation과 deterministic Go regression으로 제한된다.

## 구현 체크리스트

- [ ] [REVIEW_FAILURE_SURFACE-1] 모든 CLI launch mode의 pre-start workspace/session 오류가 반환되기 전에 `RuntimeEvent{Type: error}`로 전달되게 한다.
- [ ] [REVIEW_FAILURE_SURFACE-2] workspace preflight와 process exit regression test가 run-stream error event와 exit status/code를 강하게 검증하도록 보강한다.
- [ ] `go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai`와 `go test -count=1 ./apps/node/... ./apps/edge/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 follow-up은 같은 split subtask의 후속 루프이며 새 runtime predecessor는 없다. 구현 순서는 error-event helper 정리, launch mode 적용, regression test 보강, 검증 실행 순서다.

### [REVIEW_FAILURE_SURFACE-1] Preflight Error Event Propagation

#### 문제

`prepareWorkspaceDir` 오류는 `executeCommand`, `startProfileSession`, `startCodexAppServerProc`, `startOpencodeLocalServer`에서 process start 전에 반환된다. 그런데 `apps/node/internal/transport/session.go:45`는 `OnRunRequest` 반환 error를 Edge로 보내지 않고 로그만 남긴다. 따라서 OpenAI-compatible caller가 기다리는 `collectRunResult` 경로에는 workspace 분류가 도달하지 않는다.

#### 해결 방법

CLI adapter 실행 진입점에서 pre-start 오류를 반환하기 전에 `sink.Emit(ctx, runtime.RuntimeEvent{RunID: spec.RunID, Type: runtime.EventTypeError, Error: err.Error(), Timestamp: time.Now()})`를 보낸다. 중복 이벤트를 피하기 위해 이미 process 실행 뒤 자체 error event를 내보내는 command failure/read failure 경로는 그대로 둔다.

권장 구조:

```go
func emitReturnedError(ctx context.Context, sink runtime.EventSink, runID string, err error) error {
	if err == nil {
		return nil
	}
	_ = sink.Emit(ctx, runtime.RuntimeEvent{
		RunID:     runID,
		Type:      runtime.EventTypeError,
		Error:     err.Error(),
		Timestamp: time.Now(),
	})
	return err
}
```

적용 지점:

- `apps/node/internal/adapters/cli/oneshot.go`: `prepareWorkspaceDir` 실패.
- `apps/node/internal/adapters/cli/persistent.go`: `resolveSession` 실패 중 workspace preflight/session creation 오류.
- `apps/node/internal/adapters/cli/codex_app_server.go`: `resolveCodexAppServerSession` 실패 중 app-server start/preflight 오류.
- `apps/node/internal/adapters/cli/opencode_sse.go`: `resolveOpencodeSession` 실패 중 local server start/preflight 오류.

#### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/cli/cli.go` 또는 적절한 CLI package 파일에 반환 error event helper를 추가한다.
- [ ] `apps/node/internal/adapters/cli/oneshot.go`에서 workspace preflight error를 event로 보낸다.
- [ ] `apps/node/internal/adapters/cli/persistent.go`에서 pre-start resolve/session error를 event로 보낸다.
- [ ] `apps/node/internal/adapters/cli/codex_app_server.go`에서 app-server pre-start error를 event로 보낸다.
- [ ] `apps/node/internal/adapters/cli/opencode_sse.go`에서 opencode local server pre-start error를 event로 보낸다.

#### 테스트 작성

테스트 작성: yes. `apps/node/internal/adapters/cli/cli_internal_test.go` 또는 mode별 기존 internal test에 bad workspace 실행이 `EventTypeError`를 남기는지 확인한다.

#### 중간 검증

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

기대 결과: bad workspace direct error와 sink error event가 모두 확인되고 기존 cwd/session tests가 통과한다.

### [REVIEW_FAILURE_SURFACE-2] Regression Strengthening

#### 문제

현재 `TestCLIWorkspacePreflightFailures`는 직접 반환 error만 확인한다. `TestCLIProcessExitFailureRemainsDistinct`는 exit status/code가 없어도 `t.Logf`만 남기므로, caller가 process exit failure를 구분하는 계약을 회귀 테스트로 강제하지 못한다.

#### 해결 방법

workspace preflight tests는 sink events 안에서 `EventTypeError`와 workspace 분류 문구를 확인한다. specialized launch mode는 외부 CLI를 실제 시작하지 않도록 존재하지 않는 workspace를 사용해 preflight 단계에서 종료되게 구성한다. process-exit test는 `exit status 7` 또는 `exit code 7`이 없으면 실패해야 한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: one-shot/persistent bad workspace 실행에서 returned error와 sink `EventTypeError`를 함께 검증한다.
- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go` 또는 mode별 internal test: `codex-app-server`, `opencode-sse` bad workspace preflight도 error event를 검증한다.
- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: process exit failure가 workspace error가 아니고 exit status/code를 포함하지 않으면 테스트 실패로 처리한다.

#### 테스트 작성

테스트 작성: yes. 이번 follow-up의 핵심은 기존 unit pass가 놓친 caller-visible event surface를 고정하는 것이다.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai
```

기대 결과: CLI adapter regression과 Edge OpenAI boundary tests가 모두 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/cli/cli.go` | REVIEW_FAILURE_SURFACE-1 |
| `apps/node/internal/adapters/cli/oneshot.go` | REVIEW_FAILURE_SURFACE-1 |
| `apps/node/internal/adapters/cli/persistent.go` | REVIEW_FAILURE_SURFACE-1 |
| `apps/node/internal/adapters/cli/codex_app_server.go` | REVIEW_FAILURE_SURFACE-1 |
| `apps/node/internal/adapters/cli/opencode_sse.go` | REVIEW_FAILURE_SURFACE-1 |
| `apps/node/internal/adapters/cli/cli_internal_test.go` | REVIEW_FAILURE_SURFACE-2 |

## 최종 검증

```bash
go test -count=1 ./apps/node/internal/adapters/cli ./apps/edge/internal/openai
go test -count=1 ./apps/node/... ./apps/edge/...
git diff --check
```

기대 결과: 모든 명령이 0 exit. Go test cache output은 허용하지 않는다. `git diff --check` output은 없어야 한다.

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