<!-- task=m-iop-agent-run-bridge/01_iop_invocation_contract 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-run` Epic의 `[input]`과 `[state]`는 계약 노트로 정리되었지만, worker는 `agent_run` operation을 실제 IOP backend adapter로 넘기지 않는다. 이 작업은 concrete IOP CLI/credential 결정을 피하면서 fake IOP backend result를 받아 operation state와 durable operation event로 남기는 최소 invocation 경계를 만든다. 성공 조건은 fake backend result가 operation log에 남고 기존 non-agent operation 흐름이 깨지지 않는 것이다.

## 사용자 리뷰 요청 흐름

구현 중 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:
  - `invoke`: Gito가 직접 IOP CLI를 실행하지 않고 IOP backend invocation/result 계약을 호출하거나 수신하는 형식을 정한다.
- 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`
- `packages/contracts/README.md`
- `docs/architecture.md`
- `services/core/internal/core/model.go`
- `services/core/internal/core/model_test.go`
- `services/core/internal/events/events.go`
- `services/core/internal/worker/runner.go`
- `services/core/internal/worker/runner_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/migrations/00001_initial.sql`
- `services/core/cmd/worker/main.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/storage ./internal/core`는 현재 레이아웃에서 성공했다.
- `<확인 필요>` 값은 없었다. 외부 IOP/credential 검증은 이 plan 범위가 아니므로 사용하지 않는다.

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

- 기존 `services/core/internal/core/model_test.go`는 `agent_run` type과 operation state transition을 검증한다.
- 기존 `services/core/internal/storage/postgres_test.go`는 operation lifecycle과 event store를 검증한다.
- 공백: worker가 `agent_run` operation을 IOP adapter로 넘기고 fake backend result를 event log에 남기는 테스트가 없다. 이 plan에서 worker unit test를 추가한다.

### 심볼 참조

- renamed/removed symbol 없음.
- `NewRunner` call sites: `services/core/cmd/worker/main.go`, `services/core/internal/worker/runner_test.go`. 기존 signature는 유지한다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 task group: `agent-task/m-iop-agent-run-bridge`.
- `01_iop_invocation_contract`: 독립 subtask. fake IOP invocation/result를 operation state/event로 기록한다.
- `02+01_git_result_convergence`: `01_iop_invocation_contract` 완료 후 진행. IOP success를 Git status/revision evidence로 확정한다.
- 이 plan은 worker/storage/event 경계만 다루며, Git evidence 수집은 후속 subtask가 담당한다.

### 범위 결정 근거

- IOP CLI command, profile/model, stdin/input-file, credential injection은 Milestone 구현 잠금의 사용자 결정 항목이므로 구현하지 않는다.
- operation metadata DB schema, Flutter UI, `../agent-shell`, provider PR/MR, commit/push, dirty workspace policy는 제외한다.
- `services/core/cmd/worker/main.go`의 production wiring은 실제 store/IOP adapter 결정이 필요하므로 기존 nil wiring을 유지한다.

### 빌드 등급

- `local-G05`: worker/storage/event 경계를 건드리지만 파일 수와 call site가 작고 deterministic Go unit tests로 검증 가능하다.

## 구현 체크리스트

- [ ] worker에 IOP adapter/result 수신 경계를 추가하고 기존 `NewRunner` call site를 깨지 않는다.
- [ ] fake IOP backend result가 `agent_run` operation을 terminal state로 전환하고 operation event log를 남기는 unit test를 추가한다. 검증: `cd services/core && go test -count=1 ./internal/worker ./internal/storage ./internal/core`.
- [ ] `invoke` 범위 밖인 CLI/profile/credential/real IOP 호출은 구현하지 않고 review note에 명시한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Worker IOP Invocation Contract

#### 문제

- `services/core/internal/worker/runner.go:27`의 `Runner`는 picker와 clock만 들고 있어 IOP backend adapter나 operation event appender를 받을 수 없다.

```go
// services/core/internal/worker/runner.go:27
type Runner struct {
	cfg    config.Config
	logger *slog.Logger
	picker OperationPicker
	clock  Clock
}
```

- `services/core/internal/worker/runner.go:61` 이후는 operation을 집어 로그만 남기고, `agent_run` type을 분기하지 않는다.

```go
// services/core/internal/worker/runner.go:61
op, ok, err := r.picker.PickQueuedOperation(ctx, r.clock.Now())
if err != nil {
	r.logger.Error("failed to pick operation", "error", err)
	return fmt.Errorf("pick operation: %w", err)
}

if !ok {
	r.logger.Debug("no pending operation found")
	return nil
}

r.logger.Info("picked operation", "operation_id", op.ID, "type", op.Type)
return nil
```

- durable log primitive는 이미 있다: `services/core/internal/storage/storage.go:64`의 operation lifecycle interface와 `services/core/internal/storage/storage.go:81`의 event store.

#### 해결 방법

- `worker` package에 IOP adapter용 transport-independent types를 둔다. raw secret/log를 보관하지 않는다.

```go
type AgentRunInput struct {
	OperationID      string
	RepoID           string
	Branch           string
	WorkspacePath    string
	Instruction      string
	PolicyContext    map[string]any
	ExpectedRevision string
	CredentialRefs    map[string]string
}

type AgentRunState string
type AgentRunResult struct {
	State   AgentRunState
	Message string
}

type AgentRunInputLoader interface {
	LoadAgentRunInput(ctx context.Context, op core.Operation) (AgentRunInput, error)
}

type AgentRunInvoker interface {
	InvokeAgentRun(ctx context.Context, input AgentRunInput) (AgentRunResult, error)
}
```

- 기존 `NewRunner(cfg, logger, picker, clock)`는 유지한다. 새 constructor나 option으로 `OperationStateStore`, `OperationEventStore`, `AgentRunInputLoader`, `AgentRunInvoker`를 주입한다.
- `RunOnce`는 `op.Type != core.OperationAgentRun`이면 기존 동작을 유지한다.
- `agent_run`이면 input loader로 `AgentRunInput`을 만들고 invoker를 호출한다. result state에 따라 `SucceedOperation`, `FailOperation`, `CancelOperation` 중 하나를 호출한다.
- started/terminal operation event를 JSON object payload로 append한다. event type은 기존 `events.OperationStarted`, `events.OperationCompleted`, `events.OperationFailed`, `events.AgentRunStarted`, `events.AgentRunCompleted`만 사용한다. cancellation-specific event naming은 후속 cancellation Milestone으로 남긴다.

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

- [ ] `services/core/internal/worker/runner.go`: IOP input/result types, optional constructor, `agent_run` dispatch, operation event append helper를 추가한다.
- [ ] `services/core/internal/worker/runner_test.go`: fake loader/invoker/lifecycle/event store를 추가하고 success/failure path를 검증한다.
- [ ] `services/core/internal/events/events.go`: 새 event constant가 정말 필요할 때만 추가한다. 기본 계획은 기존 constants를 재사용한다.
- [ ] `services/core/cmd/worker/main.go`: 기존 constructor call을 깨지 않는지 확인한다. real IOP wiring은 하지 않는다.

#### 테스트 작성

- 작성: `services/core/internal/worker/runner_test.go`.
- 테스트 이름:
  - `TestRunnerRunOnceAgentRunInvokesBackendAndLogsEvents`
  - `TestRunnerRunOnceAgentRunFailsWhenBackendFails`
  - `TestRunnerRunOnceNonAgentOperationKeepsExistingBehavior`
- assertion 목표: fake backend result가 terminal operation state와 ordered operation events를 남기고, backend error는 sanitized failure event를 남긴다.

#### 중간 검증

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

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

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/worker/runner.go` | API-1 |
| `services/core/internal/worker/runner_test.go` | API-1 |
| `services/core/internal/events/events.go` | API-1 |
| `services/core/cmd/worker/main.go` | API-1 |

## 최종 검증

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

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

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