<!-- task=m-iop-agent-run-bridge/03_commit_push_policy plan=0 tag=API -->

# Plan - API

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

이 계획은 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채우는 것까지가 작업이다. 구현 에이전트는 검증을 실행하고 active 파일을 유지한 채 리뷰 준비 상태를 보고한다. 사용자만 결정할 수 있는 정책, 외부 secret, scope 충돌로 막히면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정, 근거, 실행 명령, 재개 조건을 남기고 멈춘다. 구현 중 채팅으로 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다. 최종 archive, `complete.log`, roadmap 갱신은 code-review/runtime 책임이다.

## 배경

현재 agent run 성공 판정은 Git status와 revision evidence까지 수집하지만, policy가 허용한 commit/push 결과를 operation 흐름에 연결하지 않는다. `git-result` Epic의 `[commit-push]`는 IOP 실행 결과가 실제 Git operation 결과로 수렴되는 핵심 경계다. 이 계획은 concrete IOP CLI나 secret 주입 방식을 확정하지 않고, 이미 주입된 worktree/credential 환경에서 commit/push를 수행하는 최소 control-plane 정책을 구현한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지이며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-shell-iop-execution/milestones/iop-agent-run-bridge.md`
- Task ids:
  - `commit-push`: policy가 허용한 경우 commit/push operation으로 연결한다.
- 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`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-contract/index.md`
- `services/core/internal/worker/runner.go`
- `services/core/internal/worker/runner_test.go`
- `services/core/internal/gitengine/command.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/core/model.go`
- `services/core/internal/events/events.go`
- `services/core/internal/controlplane/runtime.go`
- `services/core/internal/controlplane/runtime_test.go`
- `packages/contracts/notes/gito-control-plane.md`

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md` 있음, 읽음.
- 적용 profile: `agent-test/local/core-smoke.md`, `agent-test/local/contracts-smoke.md`, `agent-test/local/workspace-ops-smoke.md`.
- core 필수 명령: `cd services/core && go test ./...`.
- 계약 노트를 수정하면 필수 smoke: `rg --sort path -n "proto-socket|REST|gRPC|ChangeRequest|Operation|Event|agent_shell|agent-shell|iop" packages/contracts README.md docs`.
- task artifact만 수정하는 workspace-ops 검증은 `rg --sort path -n "Gito|agent-shell|agent_shell|iop|gito-shell|control plane" README.md docs packages/contracts agent-roadmap -g '!agent-roadmap/archive/**'`.
- 구조적 공백 없음. `<확인 필요>` 값 없음.

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

- `worker.Runner`가 policy 허용 시 commit/push를 호출하는 테스트가 없다. 새 fake git runner 기반 unit test가 필요하다.
- commit 실패, push 실패, push 미허용 정책 테스트가 없다. 실패 경로는 operation failed와 sanitized payload를 검증해야 한다.
- 실제 remote push integration은 secret/remote가 필요하므로 이 계획 범위에서는 unit smoke로 제한한다.

### 심볼 참조

- none. 기존 심볼 rename/remove 계획 없음.

### 분할 판단

- split decision policy를 적용했다.
- 공유 task group: `m-iop-agent-run-bridge`.
- `03_commit_push_policy`: commit/push policy와 worker/gitengine 연동. predecessor 없음.
- `04+03_revision_scan_event`: push 후 revision scan event 연결. predecessor `03_commit_push_policy`의 `complete.log` 필요.
- 두 작업은 push 성공 산출물이 scan event의 선행 조건이므로 분리한다. 기존 archive에 `01`, `02` 번호가 있어 dependency ambiguity를 피하려고 새 번호를 `03`, `04`로 잡았다.

### 범위 결정 근거

- IOP CLI 명령, profile/model, stdin/input-file 계약은 Milestone의 구현 잠금 결정 항목이라 제외한다.
- IOP credential과 workspace별 secret 주입 방식은 제외한다. 이 계획은 이미 worktree에 Git credential이 준비된 상태에서 `git push`를 실행하는 경계만 다룬다.
- provider PR/MR 생성, Flutter UI, Plane/Jira lifecycle은 Milestone 범위 제외에 따라 제외한다.
- push 후 revision scan event는 다음 subtask `04+03_revision_scan_event`로 분리한다.

### 빌드 등급

- build=`local-G06`, review=`local-G06`. core worker 정책과 git command 호출이지만 변경 범위가 작고 fake runner unit test로 검증 가능하다.

## 구현 체크리스트

- [ ] AgentRunInput `PolicyContext`에서 commit/push 허용 여부와 commit message/remote/branch를 안전하게 해석한다.
- [ ] policy가 허용한 경우에만 `gitengine.Commit`과 `gitengine.Push`를 순서대로 호출하고, 실패 시 operation failed payload를 남긴다.
- [ ] push 미허용 또는 dirty 상태에서는 기존 dirty guard 실패 동작을 유지한다.
- [ ] worker unit test로 commit+push 성공, commit 실패, push 실패, push 미허용을 검증한다.
- [ ] 계약 노트에 policy key를 추가했다면 contracts smoke를 실행한다.
- [ ] `cd services/core && go test ./internal/worker`를 실행한다.
- [ ] `cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Agent run git result policy parsing

#### 문제

[services/core/internal/worker/runner.go](/config/workspace/gito/services/core/internal/worker/runner.go:32)의 `AgentRunInput.PolicyContext`는 allowed result actions를 담을 수 있지만, 현재 [services/core/internal/worker/runner.go](/config/workspace/gito/services/core/internal/worker/runner.go:218)는 Git evidence만 수집하고 commit/push 허용 여부를 해석하지 않는다.

#### 해결 방법

`PolicyContext`에서 다음 transport-independent keys를 해석하는 작은 helper를 추가한다.

```go
// before: runner.go:218
completionExtra := map[string]any{"message": completionMessage}
if terminalState == core.OperationSucceeded {
    evidence, evidenceErr := r.collectAgentRunGitEvidence(input)
    ...
}
```

```go
// after
policy := agentRunGitResultPolicyFrom(input.PolicyContext, input.Branch)
completionExtra := map[string]any{"message": completionMessage}
if terminalState == core.OperationSucceeded {
    evidence, evidenceErr := r.collectAgentRunGitEvidence(input)
    ...
    completionExtra, terminalState, completionMessage = r.applyAgentRunGitResultPolicy(input, policy, evidence, completionMessage)
}
```

Suggested policy shape:

```go
type agentRunGitResultPolicy struct {
    CommitPushAllowed bool
    CommitMessage     string
    Remote            string
    Branch            string
}
```

Accept boolean `commit_push` or string list `allowed_result_actions` containing `commit_push`. Trim all strings. Default remote to `origin`, branch to `input.Branch`, and message to `agent run result: <operation_id>` when commit/push is allowed and no message is supplied.

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

- [ ] `services/core/internal/worker/runner.go`: policy helper와 validation 추가.
- [ ] `services/core/internal/worker/runner_test.go`: policy parsing이 observable한 success/failure 테스트 추가.
- [ ] `packages/contracts/notes/gito-control-plane.md`: 새 policy keys를 사용하면 `AgentRunInput.policy_context` 설명 아래에 transport-independent 의미만 기록.

#### 테스트 작성

작성한다. `TestRunnerRunOnceAgentRunCommitPushesWhenPolicyAllows`에서 policy key, default remote, branch fallback, commit message를 검증한다.

#### 중간 검증

```bash
cd services/core && go test ./internal/worker
```

기대: worker 패키지 테스트가 0 exit로 통과한다.

### [API-2] Commit and push execution from worker

#### 문제

[services/core/internal/gitengine/command.go](/config/workspace/gito/services/core/internal/gitengine/command.go:164)는 `Commit`, [services/core/internal/gitengine/command.go](/config/workspace/gito/services/core/internal/gitengine/command.go:177)는 `Push`를 이미 제공하지만, worker가 agent run result에 연결하지 않는다. [services/core/internal/storage/storage.go](/config/workspace/gito/services/core/internal/storage/storage.go:64)의 OperationStore는 별도 queued commit/push operation 생성을 지원하지만, worker는 현재 active agent_run의 terminal state만 처리한다.

#### 해결 방법

이번 subtask에서는 별도 queued operation을 만들지 말고, agent_run terminal handling 안에서 policy-allowed commit/push를 실행하고 payload에 결과를 남긴다. 별도 operation queue 생성은 durable orchestration을 키우므로 후속 확장으로 남긴다.

```go
// after policy and clean evidence
if policy.CommitPushAllowed {
    if err := gitengine.Commit(r.gitRunner, input.WorkspacePath, gitengine.CommitOptions{Message: policy.CommitMessage, All: true}); err != nil {
        return failedGitResultPayload(evidence, "git commit failed")
    }
    if err := gitengine.Push(r.gitRunner, input.WorkspacePath, policy.Remote, policy.Branch); err != nil {
        return failedGitResultPayload(evidence, "git push failed")
    }
}
```

Payload fields should be boring and reviewable: `commit_push_allowed`, `commit_attempted`, `push_attempted`, `push_remote`, `push_branch`, `message`. Do not record raw credential values or command stderr that may contain secrets.

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

- [ ] `services/core/internal/worker/runner.go`: commit/push execution path 추가.
- [ ] `services/core/internal/worker/runner_test.go`: git call order 검증.
- [ ] `packages/contracts/notes/gito-control-plane.md`: payload/policy wording이 필요하면 갱신.

#### 테스트 작성

작성한다. fake git runner call order는 `status`, `rev-parse`, `diff --name-status`, `add -A`, `commit -m`, `push` 순서를 기대한다. commit/push 실패 각각 operation failed와 sanitized message를 검증한다.

#### 중간 검증

```bash
cd services/core && go test ./internal/worker
```

기대: worker 패키지 테스트가 0 exit로 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/worker/runner.go` | API-1, API-2 |
| `services/core/internal/worker/runner_test.go` | API-1, API-2 |
| `packages/contracts/notes/gito-control-plane.md` | API-1, API-2 |

## 최종 검증

```bash
cd services/core && go test ./internal/worker
```

```bash
cd services/core && go test ./...
```

계약 노트를 수정했다면 추가 실행:

```bash
rg --sort path -n "proto-socket|REST|gRPC|ChangeRequest|Operation|Event|agent_shell|agent-shell|iop" packages/contracts README.md docs
```

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