<!-- task=m-iop-agent-run-bridge/04+03_revision_scan_event plan=1 tag=REVIEW_API -->

# Plan - REVIEW_API

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

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

## 배경

`04+03_revision_scan_event`의 첫 구현은 code-review에서 FAIL 판정을 받았다. 핵심 문제는 production worker entrypoint에 scanner가 주입되지 않고, worker-level scan request assertion이 부족하며, scan error payload가 raw error를 노출한다는 점이다. 이 후속 계획은 기존 구현을 살리되 Required 이슈만 고쳐 `revision-event` Task를 다시 리뷰 가능하게 만든다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 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:
  - `revision-event`: push 이후 revision scan event를 발행한다.
- 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/update-roadmap/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`
- `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-task/archive/2026/06/m-iop-agent-run-bridge/03_commit_push_policy/complete.log`
- `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/plan_local_G06_0.log`
- `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/code_review_local_G06_0.log`
- `services/core/cmd/server/main.go`
- `services/core/cmd/worker/main.go`
- `services/core/internal/config/config.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/worker/runner.go`
- `services/core/internal/worker/runner_test.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/roadmap artifact 변경 검증: `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/**'`.
- 이번 분석 중 `cd services/core && go test ./internal/worker`는 통과했다.

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

- worker production entrypoint가 scanner를 구성하는 경로가 없다.
- `TestRunnerRunOnceAgentRunRevisionScan`은 scanner 호출 횟수와 request field를 검증하지 않는다.
- push failure 또는 policy-disabled path에서 scanner가 호출되지 않는 negative assertion이 없다.
- scanner error payload가 contract의 sanitized message 조건과 일치하는지 exact assertion이 없다.

### 심볼 참조

- rename/remove 없음.
- `NewRunner(` call site: `services/core/cmd/worker/main.go`, `services/core/internal/worker/runner_test.go`.
- `RevisionScanner`/`ScanAgentRunRevision` call site: `services/core/internal/worker/runner.go`, implementation: `services/core/internal/controlplane/runtime.go`.

### 분할 판단

- split decision policy를 재평가했다.
- 공유 task group: `m-iop-agent-run-bridge`.
- 기존 sibling:
  - `01_iop_invocation_contract`: archive `complete.log` 있음.
  - `02+01_git_result_convergence`: archive `complete.log` 있음.
  - `03_commit_push_policy`: archive `complete.log` 있음.
  - `04+03_revision_scan_event`: 첫 review FAIL, 이 plan은 같은 subtask의 follow-up이다.
- 후속 작업은 첫 review Required 이슈 3개를 한 리뷰 단위로 닫는 것이 안전하다. scanner wiring, worker request assertions, sanitized message는 같은 behavior path라 분리하면 같은 테스트를 중복 수정하게 된다.

### 범위 결정 근거

- 포함: revision scanner production wiring, worker scan request/failure assertions, sanitized scan failure payload.
- 제외: IOP CLI 실행 명령, credential/secret 주입, provider PR/MR, Flutter UI, real external remote push smoke.
- 제외: dirty guard와 commit/push policy 재구현. 둘은 기존 PASS evidence와 현재 worker test로 이미 충족됐다.
- 제외: branch.updated base contract 변경. 현재 계약은 sanitized scan error와 branch.updated field semantics를 이미 요구한다.

### 빌드 등급

- build=`local-G07`, review=`local-G07`. 기존 FAIL 후속이고 cmd worker wiring, event contract, worker lifecycle 테스트가 함께 걸려 G06보다 검증 밀도가 높다.

## 구현 체크리스트

- [ ] `services/core/cmd/worker/main.go`에서 `DATABASE_URL`이 있을 때 store-backed `controlplane.Runtime`을 만들어 `RevisionScanner`로 `worker.NewRunner`에 주입한다.
- [ ] worker entrypoint 변경은 raw DSN/password를 로그나 tracked 파일에 남기지 않고, store open 실패는 기존 server main과 같은 방식으로 에러 로그 후 종료한다.
- [ ] `TestRunnerRunOnceAgentRunRevisionScan`에서 scanner call count, provider, repo_id, branch, workdir, runner, observed_at을 검증한다.
- [ ] push failure와 commit/push 미허용 path에서 scanner가 호출되지 않는 회귀 테스트를 추가한다.
- [ ] scanner error payload는 raw error를 포함하지 않는 안정적인 `"revision scan failed"` message로 고정하고, 원본 error는 로그로만 남긴다.
- [ ] `cd services/core && go test ./internal/worker ./internal/controlplane`를 실행한다.
- [ ] `cd services/core && go test ./...`를 실행한다.
- [ ] 계약 노트를 수정했다면 `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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Wire scanner into worker entrypoint

#### 문제

[services/core/cmd/worker/main.go](/config/workspace/gito/services/core/cmd/worker/main.go:15)는 `worker.NewRunner(cfg, logger, nil, nil, nil)`로 scanner를 항상 `nil`로 넘긴다. [services/core/internal/worker/runner.go](/config/workspace/gito/services/core/internal/worker/runner.go:253)의 scan path는 `r.revisionScanner != nil`일 때만 실행되므로 production worker path에서는 revision scan이 발행되지 않는다.

#### 해결 방법

`cmd/server/main.go`의 Postgres store 초기화 패턴을 worker entrypoint에도 최소 범위로 적용한다. `DATABASE_URL`이 있으면 `storage.NewPgStore`를 열고 `controlplane.NewRuntimeWithStore(nil, pgStore)`를 scanner로 넘긴다. migration SQL loader는 server main과 중복을 최소화하되, 큰 구조 변경 없이 worker command가 build되게 둔다.

```go
// before: services/core/cmd/worker/main.go:11
func main() {
	cfg := config.Load()
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	runner := worker.NewRunner(cfg, logger, nil, nil, nil)
```

```go
// after shape
var scanner worker.RevisionScanner
if dsn := os.Getenv("DATABASE_URL"); dsn != "" {
	pgStore, err := storage.NewPgStore(context.Background(), dsn, migrationSQL())
	if err != nil { ... }
	defer pgStore.Close()
	scanner = controlplane.NewRuntimeWithStore(nil, pgStore)
}
runner := worker.NewRunner(cfg, logger, nil, nil, scanner)
```

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

- [ ] `services/core/cmd/worker/main.go`: store-backed scanner construction.
- [ ] 필요하면 `services/core/cmd/server/main.go`와 migration path 후보를 맞춘다.

#### 테스트 작성

별도 cmd main unit test는 만들지 않는다. 현재 `cmd/worker`는 main package entrypoint만 있고 helper seam이 없다. build는 `cd services/core && go test ./...`의 cmd package compile로 검증한다.

#### 중간 검증

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

기대: worker/controlplane 테스트가 0 exit로 통과한다.

### [REVIEW_API-2] Assert revision scan request and negative paths

#### 문제

[services/core/internal/worker/runner_test.go](/config/workspace/gito/services/core/internal/worker/runner_test.go:1102)의 fake scanner는 request를 기록하지만, [services/core/internal/worker/runner_test.go](/config/workspace/gito/services/core/internal/worker/runner_test.go:1116) 이후 테스트는 scanner 호출 횟수와 request field를 검증하지 않는다. provider, repo_id, branch, workdir, runner, observed_at 전달이 깨져도 테스트가 통과한다.

#### 해결 방법

revision scan success case에 exact request assertions를 추가한다. `PolicyContext`에는 provider를 넣어 propagation을 검증한다. push failure와 commit/push 미허용 dirty guard path에는 scanner를 넣고도 `len(scanner.calls)==0`을 검증한다.

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

- [ ] `services/core/internal/worker/runner_test.go`: scan success request assertion.
- [ ] `services/core/internal/worker/runner_test.go`: push failure no-scan assertion.
- [ ] `services/core/internal/worker/runner_test.go`: commit/push 미허용 dirty guard no-scan assertion.

#### 테스트 작성

작성한다. 기존 `TestRunnerRunOnceAgentRunRevisionScan`, `TestRunnerRunOnceAgentRunPushFailsOperation`, `TestRunnerRunOnceAgentRunFailsWhenGitEvidenceIsDirty`를 확장하거나 명확한 하위 테스트를 추가한다.

#### 중간 검증

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

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

### [REVIEW_API-3] Sanitize scan failure payload

#### 문제

[services/core/internal/worker/runner.go](/config/workspace/gito/services/core/internal/worker/runner.go:271)는 scanner error를 `fmt.Sprintf("revision scan failed: %v", err)`로 completion payload에 넣는다. [packages/contracts/notes/gito-control-plane.md](/config/workspace/gito/packages/contracts/notes/gito-control-plane.md:115)는 scan 실패 시 sanitized scan error message를 기록한다고 되어 있어 raw error 노출은 계약과 맞지 않는다.

#### 해결 방법

payload `message`는 정확히 `"revision scan failed"`로 고정한다. 원본 error는 `logger.Warn` 필드로만 남긴다. 테스트는 raw error string이 payload에 없고 stable message만 있는지 검증한다.

```go
// before: services/core/internal/worker/runner.go:269
if err != nil {
	terminalState = core.OperationFailed
	completionMessage = fmt.Sprintf("revision scan failed: %v", err)
	completionExtra["message"] = completionMessage
```

```go
// after shape
if err != nil {
	r.logger.Warn("agent_run revision scan failed", "operation_id", op.ID, "error", err)
	terminalState = core.OperationFailed
	completionMessage = "revision scan failed"
	completionExtra["message"] = completionMessage
```

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

- [ ] `services/core/internal/worker/runner.go`: sanitized payload message and warning log.
- [ ] `services/core/internal/worker/runner_test.go`: exact message and raw error absence assertion.
- [ ] `packages/contracts/notes/gito-control-plane.md`: 변경이 필요하지 않으면 그대로 둔다.

#### 테스트 작성

작성한다. scanner error test에서 payload `message == "revision scan failed"`와 `!strings.Contains(message, "scan service down")`을 검증한다.

#### 중간 검증

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

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

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/cmd/worker/main.go` | REVIEW_API-1 |
| `services/core/internal/worker/runner.go` | REVIEW_API-3 |
| `services/core/internal/worker/runner_test.go` | REVIEW_API-2, REVIEW_API-3 |
| `packages/contracts/notes/gito-control-plane.md` | REVIEW_API-3 검토, 필요 시만 수정 |

## 최종 검증

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

```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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
