<!-- task=m-iop-agent-run-bridge/02+01_git_result_convergence plan=0 tag=API -->

# Plan - API

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

구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 둔 채 리뷰 준비 상태로 보고한다. 최종 판정, log rename, `complete.log`, task archive 이동은 code-review-skill 전용이다. 구현 중 사용자만 결정할 수 있는 IOP CLI/profile/credential 방식, 외부 secret 준비, 범위 충돌이 필요하면 review stub의 `사용자 리뷰 요청`에 증거와 재개 조건을 기록하고 멈춘다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`/`complete.log`를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

`IOP Agent Run Bridge`는 구조화된 IOP success만으로 operation을 성공 처리하지 않기로 정했다. 이 후속 작업은 `01_iop_invocation_contract`가 만든 fake IOP result path에 Git status와 before/after revision evidence를 붙인다. dirty workspace policy, commit/push, push 이후 revision event 발행은 같은 Milestone의 `git-result` Epic에 남긴다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 prompt는 금지이며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-shell-iop-execution/milestones/iop-agent-run-bridge.md`
- Task ids:
  - `result`: run 이후 Git status와 revision diff로 성공 여부를 확인한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-shell-iop-execution/PHASE.md`
- `agent-roadmap/phase/agent-shell-iop-execution/milestones/iop-agent-run-bridge.md`
- `packages/contracts/notes/gito-control-plane.md`
- `services/core/internal/worker/runner.go`
- `services/core/internal/worker/runner_test.go`
- `services/core/internal/gitengine/command.go`
- `services/core/internal/gitengine/command_test.go`
- `services/core/internal/controlplane/runtime.go`
- `services/core/internal/controlplane/runtime_test.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/storage/postgres.go`
- `services/core/internal/storage/postgres_test.go`
- `services/core/internal/storage/postgres_repo_test.go`
- `services/core/internal/core/model.go`
- `services/core/internal/core/model_test.go`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었고 core/contract/workspace-ops 라우팅이 적용된다.
- 매칭 프로필: `agent-test/local/core-smoke.md`, `agent-test/local/contracts-smoke.md`, `agent-test/local/workspace-ops-smoke.md`.
- 적용 명령: core behavior 변경은 `cd services/core && go test ./...`; 새 behavior 검증은 fresh run을 위해 `-count=1`을 사용한다.
- 확인한 명령: `cd services/core && go test -count=1 ./internal/worker ./internal/controlplane ./internal/gitengine ./internal/storage`는 현재 레이아웃에서 성공했다.
- `<확인 필요>` 값은 없었다. 실제 external IOP runner는 이 plan 범위가 아니다.

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

- 기존 `gitengine` tests는 status, diff, head revision, changed files를 검증한다.
- 기존 `controlplane` tests는 `ScanBranchRevision`이 before/after revision과 changed files를 만든다는 점을 검증한다.
- 공백: IOP result path에서 Git status/revision evidence를 operation completion payload로 붙이는 테스트가 없다. 이 plan에서 worker 중심 unit test를 추가한다.

### 심볼 참조

- renamed/removed symbol 없음.
- 이 plan은 `01_iop_invocation_contract`가 추가한 worker types를 확장할 수 있으므로, 구현 전에 active/archived predecessor의 최종 type 이름을 확인한다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 task group: `agent-task/m-iop-agent-run-bridge`.
- 현재 subtask: `02+01_git_result_convergence`.
- predecessor `01`: `agent-task/m-iop-agent-run-bridge/01_iop_invocation_contract/complete.log`가 아직 없다. 이 plan은 작성만 해두며, 구현은 predecessor PASS 후 시작해야 한다.

### 범위 결정 근거

- 이 plan은 `iop-run` Epic의 `[result]`만 완료 대상으로 둔다.
- `git-result` Epic의 `[dirty-guard]`, `[commit-push]`, `[revision-event]`는 제외한다.
- dirty workspace를 성공/실패로 최종 정책화하지 않는다. 이 plan은 evidence 수집과 operation payload 기록까지 수행하고, dirty policy는 후속 Epic에서 결정한다.
- 실제 IOP CLI/profile/credential, Flutter UI, provider PR/MR은 제외한다.

### 빌드 등급

- `local-G06`: predecessor type에 의존하고 worker/gitengine/controlplane context가 함께 필요하지만, 테스트 가능한 platformless Git evidence path로 범위를 묶을 수 있다.

## 구현 체크리스트

- [ ] `01_iop_invocation_contract`의 `complete.log`가 있는지 확인하고, 없으면 구현을 시작하지 않는다.
- [ ] IOP succeeded result path에서 Git status, before/after revision, changed files evidence를 수집해 operation completion payload에 남긴다.
- [ ] dirty workspace policy, commit/push, push 이후 revision event 발행은 구현하지 않고 후속 Epic 범위로 남긴다.
- [ ] result evidence unit test를 추가한다. 검증: `cd services/core && go test -count=1 ./internal/worker ./internal/controlplane ./internal/gitengine ./internal/storage`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 subtask directory 이름 `02+01_git_result_convergence`는 predecessor index `01`을 요구한다. 구현 전 `agent-task/m-iop-agent-run-bridge/01_iop_invocation_contract/complete.log` 또는 matching archive path의 `complete.log`를 확인한다. 현재 분석 시점에는 predecessor completion이 missing이다.

### [API-1] Git Evidence For Agent Run Result

#### 문제

- 계약 노트는 IOP `succeeded`가 Git evidence 확인 뒤에만 Gito `succeeded`가 된다고 정한다.

```markdown
<!-- packages/contracts/notes/gito-control-plane.md:89 -->
| `succeeded` | IOP reports successful execution. Gito marks `succeeded` only after the Git result confirmation step accepts workspace status and revision evidence. |
```

- `gitengine`에는 필요한 primitive가 이미 있지만 worker result path에 연결되어 있지 않다.

```go
// services/core/internal/gitengine/command.go:128
func Status(runner CommandRunner, workdir string) (StatusResult, error) {
	if strings.TrimSpace(workdir) == "" {
		return StatusResult{}, ErrInvalidGitInput
	}
	output, err := runner.Run(workdir, "status", "--porcelain")
```

```go
// services/core/internal/gitengine/command.go:189
func HeadRevision(runner CommandRunner, workdir string) (string, error) {
	if strings.TrimSpace(workdir) == "" {
		return "", ErrInvalidGitInput
	}
	output, err := runner.Run(workdir, "rev-parse", "HEAD")
```

```go
// services/core/internal/gitengine/command.go:225
func ChangedFilesWithStatus(runner CommandRunner, workdir, before, after string) ([]ChangedFile, error) {
	before = strings.TrimSpace(before)
	after = strings.TrimSpace(after)
	if strings.TrimSpace(workdir) == "" || before == "" || after == "" {
```

- `controlplane.ScanBranchRevision` already builds revision events, but it is watch/cursor-oriented rather than operation result-oriented.

#### 해결 방법

- `01_iop_invocation_contract`에서 추가된 worker result path를 확장한다.
- `AgentRunInput.ExpectedRevision`과 `AgentRunInput.WorkspacePath`가 있으면 `gitengine.Status`, `gitengine.HeadRevision`, `gitengine.ChangedFilesWithStatus`를 실행한다.
- evidence struct를 operation completion payload에 넣는다.

```go
type AgentRunGitEvidence struct {
	StatusClean    bool
	StatusOutput   string
	BeforeRevision string
	AfterRevision  string
	ChangedFiles   []gitengine.ChangedFile
}
```

- Git evidence 수집 실패는 IOP success를 신뢰하지 않고 operation failure로 기록한다.
- dirty status는 payload에 남기되, dirty policy에 따라 실패/lease dirty 처리하는 로직은 후속 `dirty-guard` Task로 남긴다.

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

- [ ] `services/core/internal/worker/runner.go`: IOP success path에 Git evidence collector를 연결한다.
- [ ] `services/core/internal/worker/runner_test.go`: fake git runner로 clean/no-change, clean/changed-revision, evidence collection failure를 검증한다.
- [ ] `services/core/internal/gitengine/command.go`: 새 helper가 꼭 필요할 때만 추가하고 기존 primitive 재사용을 우선한다.
- [ ] `services/core/internal/controlplane/runtime.go`: 직접 수정하지 않는 것을 기본으로 하되, changed-file conversion helper 재사용이 필요하면 중복을 최소화한다.

#### 테스트 작성

- 작성: `services/core/internal/worker/runner_test.go`.
- 테스트 이름:
  - `TestRunnerRunOnceAgentRunRecordsGitEvidence`
  - `TestRunnerRunOnceAgentRunFailsWhenGitEvidenceFails`
- assertion 목표: completed payload에 `status_clean`, `before_revision`, `after_revision`, `changed_files`가 있고, evidence collection error는 failed operation event로 수렴한다.

#### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/worker ./internal/controlplane ./internal/gitengine ./internal/storage
```

기대 결과: 네 package 모두 `ok`.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/worker/runner.go` | API-1 |
| `services/core/internal/worker/runner_test.go` | API-1 |
| `services/core/internal/gitengine/command.go` | API-1 |
| `services/core/internal/controlplane/runtime.go` | API-1 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/worker ./internal/controlplane ./internal/gitengine ./internal/storage
```

기대 결과: `worker`, `controlplane`, `gitengine`, `storage` package가 모두 통과한다. Go test cache output은 허용하지 않는다.

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