<!-- task=01_exit_status_contracts plan=0 tag=EXIT_STATUS_CONTRACT -->

# Plan - EXIT_STATUS_CONTRACT

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

**구현의 마지막 단계는 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 모두 채우는 것이다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
아래 `구현 체크리스트` 순서대로 구현하고 중간/최종 검증을 실행한 뒤, 실제 구현 내용과 stdout/stderr를 `CODE_REVIEW-cloud-G07.md`에 기록한다.
리뷰 파일의 아카이브 지시와 `코드리뷰 전용 체크리스트`는 코드리뷰 에이전트 전용이므로 실행하거나 수정하지 않는다.

## 배경

Git/GitHub 커맨드 일부가 exit code `1`을 넓게 성공으로 취급한다.
현재 구조에서는 인증 실패, push reject, gh CLI 오류도 성공 파이프라인으로 흐를 수 있다.
외부 CLI exit-status 계약은 실제 자동화 실패 전파에 직접 영향을 주므로 별도 계획으로 격리한다.

## 분석 결과

### 읽은 파일

- `lib/oto/commands/command.dart`
- `lib/oto/commands/git/git_remote.dart`
- `lib/oto/commands/git/git_hub.dart`
- `lib/oto/data/git_data.dart`
- `test/oto_command_runtime_test.dart`

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

- `GitPull`/`GitPush` exit code `1` 실패 전파: 기존 테스트 없음. 회귀 테스트 필요.
- `GitHubPullRequestCreate`의 "이미 PR 존재" exit code `1`만 성공 처리: 기존 테스트 없음. 성공/실패 양쪽 테스트 필요.
- `FakeRuntime.start()` stderr 전달: 기존 테스트 없음. GitHub/Git 실패 테스트를 위해 보강 필요.

### 심볼 참조

- 이름 변경/삭제 없음.
- `passExitCodes` 참조: `lib/oto/commands/command.dart`, `lib/oto/commands/git/git_remote.dart`, `lib/oto/commands/git/git_hub.dart`, `lib/oto/commands/git/git_stash.dart`, `lib/oto/commands/build/testflight_ios.dart`.

### 분할 판단

분할 정책을 먼저 평가했다.
이 작업은 Git/GitHub exit-status 계약만 다루며 BuildFlutter 계약과 독립적이다.
외부 CLI exit-status 동작이므로 `02_build_flutter_contract`와 분리한다.
형제 task: `01_exit_status_contracts` 독립, `02_build_flutter_contract` 독립.

### 범위 결정 근거

- 포함: `GitPull`, `GitPush`, `GitHubPullRequestCreate`, 테스트 fake stderr 전달.
- 제외: `GitStashApply`, TestFlight 계열의 `passExitCodes: [1]`는 도메인별 의미가 달라 별도 검토가 필요하다.
- 제외: 공통 `Command.completeProcess()` 의미 변경은 모든 커맨드에 영향을 주므로 이번 범위에서 다루지 않는다.

### 빌드 등급

`cloud-G07`: 외부 CLI exit-status 계약, stderr/stdout 파싱, 실패 전파 동작을 다룬다.

## 구현 체크리스트

- [ ] `GitPull`/`GitPush`가 exit code `1`을 기본 성공 처리하지 않도록 수정하고 회귀 테스트를 추가한다.
- [ ] `GitHubPullRequestCreate`가 기존 PR 감지 메시지일 때만 exit code `1`을 성공 처리하도록 수정하고 회귀 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [EXIT_STATUS_CONTRACT-1] GitPull/GitPush exit code 1 실패 전파

#### 문제

`lib/oto/commands/git/git_remote.dart:55`와 `lib/oto/commands/git/git_remote.dart:76`이 `passExitCodes: [1]`을 전달한다.
`lib/oto/commands/command.dart:281`은 pass exit code에 포함된 비영 exit code를 성공으로 끝낸다.

#### 해결 방법

`GitPull`/`GitPush`에서 `passExitCodes: [1]`을 제거한다.

Before (`lib/oto/commands/git/git_remote.dart:54`):
```dart
var process = await runtime.start(shell);
return await completeProcess(process, command,
    passExitCodes: [1 /*Everything up-to-date*/]);
```

After:
```dart
var process = await runtime.start(shell);
return await completeProcess(process, command);
```

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

- [ ] `lib/oto/commands/git/git_remote.dart`: `GitPull`, `GitPush`의 넓은 pass exit code 제거.
- [ ] `test/oto_command_runtime_test.dart`: exit code `1`이 예외로 전파되는 테스트 추가.

#### 테스트 작성

작성한다.
`test/oto_command_runtime_test.dart`에 `git_pull propagates exit code 1 as failure`, `git_push propagates exit code 1 as failure` 테스트를 추가한다.
`FakeRuntime(exitCode: 1, stderrText: 'remote rejected')`로 실행하고 `throwsA`를 검증한다.

#### 중간 검증

```bash
dart test test/oto_command_runtime_test.dart
```
기대 결과: Git/GitHub 런타임 테스트 전체 통과.

### [EXIT_STATUS_CONTRACT-2] GitHubPullRequestCreate exit code 1 좁은 성공 처리

#### 문제

`lib/oto/commands/git/git_hub.dart:40`은 `gh pr create`의 exit code `1`을 "already exists"로 가정한다.
같은 파일 `44-47`은 stderr에 콜론이 없거나 PR URL이 없을 때도 문자열을 잘라 성공 경로로 들어갈 수 있다.

#### 해결 방법

exit code `0`은 stdout URL을 기존처럼 처리한다.
exit code `1`은 stderr에서 기존 PR URL을 식별한 경우에만 `passExitCodes: [1]`로 완료한다.
식별 실패 시 `completeProcess(process, command)`로 실패를 전파한다.

Before (`lib/oto/commands/git/git_hub.dart:40`):
```dart
var passExitCodes = [0, 1 /*already exists*/];
var exitCode = process.exitCode ?? await process.process.exitCode;
if (passExitCodes.contains(exitCode)) {
```

After:
```dart
var exitCode = process.exitCode ?? await process.process.exitCode;
if (exitCode == 0) {
  // stdout URL 처리
} else if (exitCode == 1 && _extractPullRequestUrl(process.stderr.toString()) != null) {
  // 기존 PR URL 처리 후 passExitCodes: [1]
} else {
  return await completeProcess(process, command);
}
```

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

- [ ] `lib/oto/commands/git/git_hub.dart`: 기존 PR URL 추출 헬퍼를 추가한다.
- [ ] `lib/oto/commands/git/git_hub.dart`: exit code `1` 성공 조건을 기존 PR URL 감지로 제한한다.
- [ ] `test/oto_command_runtime_test.dart`: `FakeRuntime.start()`가 stderr를 `FakeProcessData`에 전달하도록 보강한다.
- [ ] `test/oto_command_runtime_test.dart`: 기존 PR 감지 성공 테스트와 인증 실패 전파 테스트를 추가한다.

#### 테스트 작성

작성한다.
`github_pull_request_create treats existing-pr message as success`는 stderr에 `https://github.com/org/repo/pull/123`을 넣고 `property`의 URL/number 저장을 검증한다.
`github_pull_request_create propagates unrelated exit code 1`은 stderr `authentication failed`를 넣고 예외 전파를 검증한다.

#### 중간 검증

```bash
dart test test/oto_command_runtime_test.dart
```
기대 결과: GitHub PR create 성공/실패 회귀 테스트 포함 전체 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/commands/git/git_remote.dart` | EXIT_STATUS_CONTRACT-1 |
| `lib/oto/commands/git/git_hub.dart` | EXIT_STATUS_CONTRACT-2 |
| `test/oto_command_runtime_test.dart` | EXIT_STATUS_CONTRACT-1, EXIT_STATUS_CONTRACT-2 |

## 최종 검증

```bash
dart test test/oto_command_runtime_test.dart
dart analyze
```
기대 결과: 두 명령 모두 성공. Dart test cache 개념은 적용하지 않는다.

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