<!-- task=m-platformless-git-operation-engine plan=1 tag=REVIEW_SCAN -->

# Plan - REVIEW_SCAN

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

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

## 배경

첫 리뷰에서 `ScanBranchRevision`의 changed scan이 branch watch가 없을 때 cursor를 전진시키지 않는 결함이 확인되었다. 이 결함은 first observation 이후 같은 unwatched repo/branch의 변경 range를 반복 처리하게 만들어 `cursor` Roadmap Target을 충족하지 못한다. 후속 작업은 no-watch changed scan의 cursor 저장 semantics와 회귀 테스트만 다룬다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 active `CODE_REVIEW-local-G06.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:
  - `cursor`: repo/branch별 last seen revision cursor를 저장한다.
  - `event-input`: scan 결과를 `RevisionEvent` 후보로 변환한다.
- 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/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-task/m-platformless-git-operation-engine/plan_local_G06_0.log`
- `agent-task/m-platformless-git-operation-engine/code_review_local_G06_0.log`
- `services/core/internal/controlplane/runtime.go`
- `services/core/internal/controlplane/runtime_test.go`
- `services/core/internal/gitengine/command.go`
- `services/core/internal/core/model.go`
- `services/core/internal/storage/storage.go`

### 테스트 환경 규칙

- `test_env`: `local`
- `agent-test/local/rules.md`: 존재하며 읽음.
- 읽은 profile:
  - `agent-test/local/core-smoke.md`: core 변경은 최소 `cd services/core && go test ./...`로 확인한다. gitengine/storage/controlplane behavior 변경은 해당 동작을 검증하는 테스트가 필요하다.
  - `agent-test/local/workspace-ops-smoke.md`: task artifact와 roadmap 변경은 `git diff --check`로 확인한다.
- 적용 명령:
  - focused: `cd services/core && go test -count=1 ./internal/controlplane -run TestRuntimeScanBranchRevisionBuildsRevisionEvent`
  - focused suite: `cd services/core && go test -count=1 ./internal/controlplane -run TestRuntimeScanBranchRevision`
  - core smoke: `cd services/core && go test ./...`
  - workspace preflight: `git diff --check`
- `<확인 필요>` 값: 없음.
- fallback verification: 없음.

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

- 기존 `TestRuntimeScanBranchRevisionBuildsRevisionEvent`는 no-watch changed scan이 event를 emit하지 않는지만 확인한다. 해당 scan 후 `revision_cursors`가 latest head로 전진했는지 검증하지 않아 이번 결함을 놓친다.
- watched changed scan, first observation, unchanged scan, changed file status 보존은 기존 테스트가 검증한다.

### 심볼 참조

- renamed/removed symbol: none.
- relevant symbols:
  - `services/core/internal/controlplane/runtime.go:186` `handleRevisionWithStore`
  - `services/core/internal/controlplane/runtime.go:324` `ScanBranchRevision`
  - `services/core/internal/controlplane/runtime.go:389` changed scan의 `HandleRevision` 호출
  - `services/core/internal/controlplane/runtime_test.go:442` `TestRuntimeScanBranchRevisionBuildsRevisionEvent`

### 분할 판단

split policy를 평가했다. 수정은 `ScanBranchRevision`의 unmatched changed path와 단일 회귀 assertion에 한정되고, 별도 API foundation이나 broad rollout이 없다. 같은 controlplane 테스트 하나로 검증되므로 단일 follow-up plan이 가장 작고 안전하다.

### 범위 결정 근거

- 포함: `services/core/internal/controlplane/runtime.go`, `services/core/internal/controlplane/runtime_test.go`
- 제외: provider webhook no-watch semantics, Postgres migration, gitengine parser, REST route, roadmap 문서 갱신. 이번 후속은 첫 리뷰 Required 결함만 고친다.

### 빌드 등급

- build lane/grade: `local-G06`
- rationale: explicit deterministic correctness bug이며 변경 파일과 회귀 검증이 bounded core 내부 작업이다.

## 구현 체크리스트

- [ ] no-watch changed scan이 event를 emit하지 않더라도 repo/branch cursor를 latest head로 저장하게 수정하고 회귀 테스트로 검증한다.
- [ ] `cd services/core && go test -count=1 ./internal/controlplane -run TestRuntimeScanBranchRevisionBuildsRevisionEvent`, `cd services/core && go test -count=1 ./internal/controlplane -run TestRuntimeScanBranchRevision`, `cd services/core && go test ./...`, `git diff --check`를 실행하고 결과를 review stub에 붙인다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_SCAN-1] No-Watch Changed Scan Cursor Advance

문제: `services/core/internal/controlplane/runtime.go:389`에서 changed scan을 `HandleRevision(ctx, provider, "", revEvent)`에 그대로 위임한다. store-backed `HandleRevision`은 `services/core/internal/controlplane/runtime.go:187`에서 branch watch를 찾고, watch가 없으면 `services/core/internal/controlplane/runtime.go:192`에서 cursor 저장 전에 반환한다. 따라서 first observation으로 cursor가 생긴 no-watch scan target에 새 commit이 생기면 cursor가 old revision에 남아 같은 range를 반복 처리한다.

해결 방법: `ScanBranchRevision`에서 `HandleRevision` 결과를 받은 뒤 `matched=false`이면 scan target cursor를 `after` revision으로 저장한다. `matched=true` 경로는 기존 `HandleRevision`의 cursor 저장을 그대로 사용해 중복 저장을 피한다.

Before:
```go
// services/core/internal/controlplane/runtime.go:388
revEvent := revisionEventFromScan(repoID, branch, before, after, files, observedAt)
return r.HandleRevision(ctx, provider, "", revEvent)
```

After:
```go
record, matched, err := r.HandleRevision(ctx, provider, "", revEvent)
if err != nil {
	return record, matched, err
}
if !matched {
	if err := r.store.RevisionCursors().UpsertRevisionCursor(ctx, core.RevisionCursor{
		RepoID: repoID, Branch: branch, Revision: after, ObservedAt: observedAt,
	}); err != nil {
		return record, false, fmt.Errorf("upsert revision cursor: %w", err)
	}
}
return record, matched, nil
```

수정 파일 및 체크리스트:
- [ ] `services/core/internal/controlplane/runtime.go`: changed scan unmatched path에서 cursor를 latest head로 저장한다.
- [ ] `services/core/internal/controlplane/runtime_test.go`: `TestRuntimeScanBranchRevisionBuildsRevisionEvent`의 nowatch second scan 뒤 cursor revision이 새 head와 일치하는 assertion을 추가한다.

테스트 작성: 작성한다. Test name은 기존 `TestRuntimeScanBranchRevisionBuildsRevisionEvent`를 확장한다. fixture는 기존 temp local git repo와 `fakeStore`를 사용하고, no-watch changed scan 후 `store.cursors.GetRevisionCursor(ctx, "nowatch", "main")`가 latest head를 반환해야 한다.

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

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/controlplane/runtime.go` | REVIEW_SCAN-1 |
| `services/core/internal/controlplane/runtime_test.go` | REVIEW_SCAN-1 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/controlplane -run TestRuntimeScanBranchRevisionBuildsRevisionEvent
```

```bash
cd services/core && go test -count=1 ./internal/controlplane -run TestRuntimeScanBranchRevision
```

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

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

기대 결과: 모든 명령이 exit code 0으로 종료한다. focused controlplane 명령은 fresh execution이 필요하고, `go test ./...`의 cache output은 허용한다.

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