<!-- task=m-platformless-git-operation-engine plan=0 tag=OPS -->

# Plan - OPS

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

`CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수 마지막 단계다. 구현 후 검증 명령을 실행하고, active 파일은 그대로 둔 채 리뷰 준비 상태를 보고한다. 종결 판정, 로그 아카이브, `complete.log` 작성, task directory archive 이동은 code-review 스킬 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경, scope conflict 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 기록하고 멈춘다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`, `complete.log`, archive 로그를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 마일스톤의 `ops` 에픽은 Git CLI와 local bare repo만으로 clone/fetch, status/diff, commit/push, branch checkout을 검증해야 한다. 기존 `gitengine`은 명령 래퍼와 fake runner 단위 테스트가 있으나, 로드맵의 `검증:` 문구가 요구하는 실제 local bare repo 기반 증거가 부족하다. pre-plan 작은 처리로 `Diff` 래퍼와 fake runner 단위 테스트는 추가되었으므로, 이 plan은 남은 통합 검증과 source branch 기준 checkout API를 완성한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 active `CODE_REVIEW-local-G05.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사된 형식이며, 구현 중 direct user prompt는 금지된다. code-review가 차단 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-foundation/milestones/platformless-git-operation-engine.md`
- Task ids:
  - `clone-fetch`: clone/fetch operation을 구현한다.
  - `status-diff`: status/diff operation을 구현한다.
  - `commit-push`: commit/push operation을 구현한다.
  - `branch-checkout`: branch checkout과 source branch 기준을 구현한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/control-plane-foundation/PHASE.md`
- `agent-roadmap/phase/control-plane-foundation/milestones/platformless-git-operation-engine.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `.gitignore`
- `services/core/go.mod`
- `services/core/internal/core/model.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/gitengine/command.go`
- `services/core/internal/gitengine/command_test.go`

### 테스트 환경 규칙

- `test_env`: `local`
- `agent-test/local/rules.md`: 존재하며 읽음. core 변경은 `core-smoke`, agent-task artifact 변경은 `workspace-ops-smoke` 라우팅에 해당한다.
- 읽은 profile:
  - `agent-test/local/core-smoke.md`: core 코드 변경은 최소 `cd services/core && go test ./...`로 확인한다. gitengine behavior 변경은 해당 동작을 검증하는 테스트가 필요하다.
  - `agent-test/local/workspace-ops-smoke.md`: docs/roadmap/task artifact 변경은 `git diff --check`를 사용한다.
- 적용 명령:
  - focused: `cd services/core && go test -count=1 ./internal/gitengine`
  - core smoke: `cd services/core && go test ./...`
  - workspace preflight: `git diff --check`
- `<확인 필요>` 값: 없음.
- fallback verification: 없음. local 규칙과 profile이 명확하다.

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

- `clone-fetch`: `TestCloneFetchCheckout`는 fake runner command construction만 검증한다. local bare repo remote로 clone/fetch 후 revision 확인이 없다.
- `status-diff`: `Status`와 pre-plan `Diff` fake runner tests는 명령 인자만 검증한다. clean, unstaged, staged 상태를 실제 worktree에서 확인하는 테스트가 없다.
- `commit-push`: fake runner test만 있으며, local bare repo remote branch에 push revision이 반영되는지 검증하지 않는다.
- `branch-checkout`: 기존 `Checkout`은 `git checkout <branch>`만 감싸며 `source branch`/start-point 기준 branch 생성 또는 reset API가 없다. 실제 branch 변경 후 worktree revision 확인도 없다.

### 심볼 참조

- renamed/removed symbol: none.
- added symbol already present before this plan: `Diff`, `DiffOptions` in `services/core/internal/gitengine/command.go`; references are limited to `services/core/internal/gitengine/command_test.go`.

### 분할 판단

분할 정책을 plan file 선택 전에 평가했다. split 후보는 roadmap task 네 개였지만 모두 `services/core/internal/gitengine/command.go`와 `command_test.go` 한 ownership boundary 안에서 구현된다. local bare repo test helper를 각 subtask가 중복하거나 같은 test file을 순차적으로 수정하게 되어 coordination overhead가 더 크다. API/foundation과 broad call-site rollout 경계가 없고, external provider/storage/controlplane 변경도 제외되어 전체 변경은 한 리뷰에서 판단 가능하다. 따라서 단일 plan을 `agent-task/m-platformless-git-operation-engine/`에 둔다.

### 범위 결정 근거

- 포함: `services/core/internal/gitengine/command.go`, `services/core/internal/gitengine/command_test.go`.
- 제외: `services/core/internal/storage/**`, `services/core/internal/controlplane/**`, `packages/contracts/**`, provider adapter, operation DTO, timeout/stderr structured result, `scan` 에픽. 이 plan은 사용자가 요청한 현재 마일스톤의 `ops` 에픽 완료에 한정한다.
- 기존 작업트리에는 storage/roadmap/contracts 변경이 있으나 이 plan의 구현 대상이 아니므로 수정하지 않는다.

### 빌드 등급

- build lane/grade: `local-G05`
- rationale: core 내부 한 패키지의 bounded 변경이며 검증은 local git binary와 Go tests로 결정 가능하지만, 실제 bare repo integration과 branch start-point semantics가 있어 중간 등급으로 둔다.

## 구현 체크리스트

- [ ] local bare repo test helper를 `command_test.go`에 추가하고 clone/fetch integration이 remote revision을 확인하게 한다.
- [ ] status/diff integration test를 추가해 clean, unstaged, staged 상태와 `Diff`/`DiffOptions{Staged:true}` 출력을 확인한다.
- [ ] commit/push integration test를 추가해 local git user config를 설정한 뒤 remote branch revision 반영을 확인한다.
- [ ] source branch 기준 checkout API와 단위/integration test를 추가해 branch 변경 후 worktree revision을 확인한다.
- [ ] `cd services/core && go test -count=1 ./internal/gitengine`, `cd services/core && go test ./...`, `git diff --check`를 실행하고 결과를 review stub에 붙인다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [OPS-1] Clone/Fetch Local Bare Repo Integration

문제: `Clone`/`Fetch`는 wrapper 자체는 있으나 `services/core/internal/gitengine/command_test.go:92`의 fake runner 테스트만 있어 milestone의 local bare repo 검증을 충족하지 못한다.

해결 방법: `command_test.go`에 temp dir 기반 helper를 추가한다. helper는 `git init --bare`, seed repo init, local user config, initial commit, push를 수행하고 remote path, seed path, expected revision을 반환한다. `TestCloneFetchWithLocalBareRemote`를 추가해 `Clone(CLI{}, CloneOptions{Branch:"main"})`, seed commit 추가, `Fetch(CLI{}, worktree, "")`, `rev-parse origin/main` 확인을 수행한다.

수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitengine/command_test.go`: `os`, `os/exec`, `path/filepath`, `strings` import를 추가한다.
- [ ] `services/core/internal/gitengine/command_test.go`: `runGit`, `gitOutput`, `writeFile`, `setupBareRemote` test helper를 추가한다.
- [ ] `services/core/internal/gitengine/command_test.go`: `TestCloneFetchWithLocalBareRemote`를 추가한다.

테스트 작성: 작성한다. Test name은 `TestCloneFetchWithLocalBareRemote`; assertion은 fetched `origin/main` revision이 bare remote `main` revision과 같은지 확인한다. fixture는 `t.TempDir()` 하위 bare repo와 seed/worktree repo다.

중간 검증:
```bash
cd services/core && go test -count=1 ./internal/gitengine -run TestCloneFetchWithLocalBareRemote
```
기대 결과: `ok git.toki-labs.com/toki/gito/services/core/internal/gitengine`.

### [OPS-2] Status/Diff Integration States

문제: `Status`는 `services/core/internal/gitengine/command.go:90`에서 porcelain status를 반환하고, `Diff`는 `services/core/internal/gitengine/command.go:104`에서 unstaged/staged diff를 감싼다. 그러나 `services/core/internal/gitengine/command_test.go:37`과 `:51` 주변 fake runner tests만 있어 clean, unstaged, staged 실제 worktree 상태를 보장하지 않는다.

해결 방법: `TestStatusDiffWithCleanStagedUnstagedWorkspace`를 추가한다. helper가 만든 clone worktree에서 처음에는 `Status.Clean == true`를 확인한다. 파일을 수정한 뒤 `Status.Clean == false`와 `Diff(CLI{}, worktree, DiffOptions{})`에 수정 내용이 포함되는지 확인한다. `git add` 후 `Diff(CLI{}, worktree, DiffOptions{Staged:true})`가 staged 내용을 포함하고 unstaged diff가 비어 있음을 확인한다.

수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitengine/command_test.go`: status/diff integration test를 추가한다.
- [ ] `services/core/internal/gitengine/command_test.go`: diff assertion helper는 test-local helper로만 둔다.

테스트 작성: 작성한다. Test name은 `TestStatusDiffWithCleanStagedUnstagedWorkspace`; assertion은 clean status, dirty status, unstaged diff, staged diff를 각각 확인한다.

중간 검증:
```bash
cd services/core && go test -count=1 ./internal/gitengine -run TestStatusDiffWithCleanStagedUnstagedWorkspace
```
기대 결과: `ok git.toki-labs.com/toki/gito/services/core/internal/gitengine`.

### [OPS-3] Commit/Push Remote Revision

문제: `Commit`/`Push`는 `services/core/internal/gitengine/command.go:126`과 `:139`에 wrapper가 있지만, `services/core/internal/gitengine/command_test.go:118`의 fake runner test만 있다. 실제 local bare remote branch revision 반영을 검증하지 않는다.

해결 방법: `TestCommitPushToLocalBareRemote`를 추가한다. helper로 clone한 worktree에서 local user config를 설정하고 파일을 수정한다. `Commit(CLI{}, worktree, CommitOptions{Message:"...", All:true})` 후 `Push(CLI{}, worktree, "", "main")`를 실행한다. `HeadRevision(CLI{}, worktree)`와 bare remote `main` revision이 같은지 확인한다.

수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitengine/command_test.go`: `TestCommitPushToLocalBareRemote`를 추가한다.
- [ ] `services/core/internal/gitengine/command_test.go`: test helper가 local git user config를 항상 설정하게 한다.

테스트 작성: 작성한다. Test name은 `TestCommitPushToLocalBareRemote`; assertion은 worktree HEAD와 bare remote branch revision equality다.

중간 검증:
```bash
cd services/core && go test -count=1 ./internal/gitengine -run TestCommitPushToLocalBareRemote
```
기대 결과: `ok git.toki-labs.com/toki/gito/services/core/internal/gitengine`.

### [OPS-4] Source Branch Checkout API

문제: `Checkout`은 `services/core/internal/gitengine/command.go:82`에서 `git checkout <branch>`만 실행한다. 마일스톤 task의 "source branch 기준"을 표현할 input이 없어 `feature` branch를 `main` 또는 다른 revision에서 생성/초기화할 수 없다.

해결 방법: 기존 `Checkout`은 호환 wrapper로 유지하고, `CheckoutOptions`와 `CheckoutBranch`를 추가한다. `StartPoint`가 비어 있으면 기존 checkout과 동일하게 `git checkout <branch>`를 호출한다. `StartPoint`가 있으면 기본 `git checkout -b <branch> <start-point>`를 호출하고, `Reset`이 true면 `git checkout -B <branch> <start-point>`를 호출한다.

Before:
```go
// services/core/internal/gitengine/command.go:82
func Checkout(runner CommandRunner, workdir, branch string) error {
	if strings.TrimSpace(workdir) == "" || strings.TrimSpace(branch) == "" {
		return ErrInvalidGitInput
	}
	_, err := runner.Run(workdir, "checkout", branch)
	return err
}
```

After:
```go
type CheckoutOptions struct {
	Branch     string
	StartPoint string
	Reset      bool
}

func Checkout(runner CommandRunner, workdir, branch string) error {
	return CheckoutBranch(runner, workdir, CheckoutOptions{Branch: branch})
}

func CheckoutBranch(runner CommandRunner, workdir string, opts CheckoutOptions) error {
	workdir = strings.TrimSpace(workdir)
	branch := strings.TrimSpace(opts.Branch)
	if workdir == "" || branch == "" {
		return ErrInvalidGitInput
	}
	startPoint := strings.TrimSpace(opts.StartPoint)
	if startPoint == "" {
		_, err := runner.Run(workdir, "checkout", branch)
		return err
	}
	flag := "-b"
	if opts.Reset {
		flag = "-B"
	}
	_, err := runner.Run(workdir, "checkout", flag, branch, startPoint)
	return err
}
```

수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitengine/command.go`: `CheckoutOptions`와 `CheckoutBranch`를 추가하고 기존 `Checkout`을 delegating wrapper로 유지한다.
- [ ] `services/core/internal/gitengine/command_test.go`: fake runner 단위 테스트에서 `CheckoutBranch` 인자를 검증한다.
- [ ] `services/core/internal/gitengine/command_test.go`: `TestCheckoutBranchFromSource` integration test를 추가한다.

테스트 작성: 작성한다. Test names는 `TestCheckoutBranchFromSourceArgs`와 `TestCheckoutBranchFromSource`; assertion은 source branch/start point revision에서 새 branch가 만들어지고 worktree HEAD가 source revision과 같음을 확인한다.

중간 검증:
```bash
cd services/core && go test -count=1 ./internal/gitengine -run 'TestCheckoutBranchFromSource|TestCloneFetchCheckout'
```
기대 결과: `ok git.toki-labs.com/toki/gito/services/core/internal/gitengine`.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/gitengine/command.go` | OPS-4 |
| `services/core/internal/gitengine/command_test.go` | OPS-1, OPS-2, OPS-3, OPS-4 |

## 최종 검증

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

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

```bash
git diff --check
```

기대 결과: 모든 명령이 exit code 0으로 종료한다. `go test ./...`는 Go cache output을 허용한다. `go test -count=1 ./internal/gitengine`은 gitengine integration test를 fresh execution으로 검증한다.

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