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

# Plan - REVIEW_REVIEW_API

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

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

## 배경

G07은 scanner injection, request assertion, sanitized scan failure를 보완했지만 worker scanner가 `nil` broadcaster runtime을 사용한다. 이 상태에서는 `revision_event_matched=true`가 branch.updated proto-socket event publication을 의미한다는 계약을 충족하지 못한다. 이 후속 계획은 scan matched와 event published를 같은 success boundary로 묶어 false-positive completion payload를 막는다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 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

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/plan_local_G07_1.log`
- Current archived review: `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/code_review_local_G07_1.log`
- Verdict: FAIL
- Required summary:
  - `services/core/cmd/worker/main.go:58` constructs `controlplane.NewRuntimeWithStore(nil, pgStore)`, so `services/core/internal/controlplane/runtime.go:238` cannot broadcast `branch.updated`.
  - `packages/contracts/notes/gito-control-plane.md:111` describes `revision_event_matched` as generated/published event evidence.
  - `agent-contract/provided/gito-forgejo-branch-events-v1.md:190` defines proto-socket event channel broadcast as the internal consumer path.
- Affected files:
  - `services/core/cmd/worker/main.go`
  - `services/core/internal/controlplane/runtime.go`
  - `services/core/internal/controlplane/runtime_test.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`
  - `packages/contracts/notes/gito-control-plane.md`
  - `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- Verification evidence:
  - `cd services/core && go test -count=1 ./internal/worker ./internal/controlplane` passed during review.
  - `cd services/core && go test ./...` passed during review.
  - `rg --sort path -n "proto-socket|REST|gRPC|ChangeRequest|Operation|Event|agent_shell|agent-shell|iop" packages/contracts README.md docs` passed during review.
  - `git diff --check` passed during review.
- Allowed archive reread:
  - `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/code_review_local_G07_1.log`
  - `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/plan_local_G07_1.log`

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/PLAN-local-G07.md`
- `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/CODE_REVIEW-local-G07.md`
- `agent-task/m-iop-agent-run-bridge/04+03_revision_scan_event/code_review_local_G06_0.log`
- `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `services/core/cmd/worker/main.go`
- `services/core/internal/worker/runner.go`
- `services/core/internal/worker/runner_test.go`
- `services/core/internal/controlplane/runtime.go`
- `services/core/internal/controlplane/router.go`
- `services/core/internal/controlplane/runtime_test.go`
- `services/core/internal/storage/storage.go`
- `services/core/internal/storage/postgres.go`
- `services/core/internal/events/events.go`
- `services/core/migrations/00001_initial.sql`
- `packages/contracts/notes/gito-control-plane.md`
- `docs/architecture.md`
- `README.md`

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md` 있음, 읽음.
- 적용 profile: `agent-test/local/core-smoke.md`, `agent-test/local/contracts-smoke.md`.
- core 필수 명령: `cd services/core && go test ./...`.
- protocol/contract 문구 확인 smoke: `rg --sort path -n "proto-socket|REST|gRPC|ChangeRequest|Operation|Event|agent_shell|agent-shell|iop" packages/contracts README.md docs`.
- fresh focused command used by review: `cd services/core && go test -count=1 ./internal/worker ./internal/controlplane`.
- 구조적 공백 없음. `<확인 필요>` 값 없음.

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

- worker revision scan tests verify scanner call and payload fields, but do not prove `branch.updated` is broadcast or stored in a consumer-visible event boundary.
- controlplane runtime tests cover in-memory/runtime scan records and cursor behavior, but not worker scan publishing through a non-nil broadcaster or durable outbox.
- cmd worker build test compiles `NewRuntimeWithStore(nil, pgStore)` but does not catch missing broadcaster.

### 심볼 참조

- rename/remove 없음.
- Relevant references checked with `rg --sort path -n "branchUpdatedEnvelope|BroadcastEnvelope|HandleRevision\\(|ScanBranchRevision\\(|ListEvents\\(" services/core/internal/controlplane services/core/internal -g '*.go'`.

### 분할 판단

- split decision policy를 적용했다.
- 공유 task group: `m-iop-agent-run-bridge`.
- 현재 subtask: `04+03_revision_scan_event`.
- predecessor `03_commit_push_policy`는 `agent-task/archive/2026/06/m-iop-agent-run-bridge/03_commit_push_policy/complete.log`로 이미 충족됐다.
- 이 follow-up은 같은 event boundary의 한 Required 이슈를 닫는 작업이다. runtime publish boundary, worker payload semantics, tests가 한 행동 단위라 별도 subtask로 나누지 않는다.

### 범위 결정 근거

- 포함: worker revision scan success가 실제 branch.updated event publish/storage boundary를 통과했다는 증거를 만들고, payload가 그 사실만 `revision_event_matched=true`로 기록하게 한다.
- 포함: nil broadcaster runtime이 matched success로 보이는 경로를 실패 또는 explicit non-published result로 바꾼다.
- 제외: external Forgejo remote smoke, IOP CLI, credential/secret injection, Flutter UI.
- 제외: unrelated operation lifecycle event schema redesign. 필요한 storage/outbox 보완은 branch.updated publish proof에 필요한 최소 범위로 제한한다.

### 빌드 등급

- build=`cloud-G07`, review=`cloud-G07`. 반복 FAIL이고 branch.updated protocol, runtime broadcaster, storage/outbox 경계가 함께 걸려 local보다 강한 cross-boundary 검토가 필요하다.

## 구현 체크리스트

- [ ] worker revision scan success가 `branch.updated`를 consumer-visible boundary에 publish/store하지 못하면 `revision_event_matched=true`를 기록하지 않도록 한다.
- [ ] worker에서 쓰는 scanner runtime에 non-nil event publishing boundary를 주입하거나, durable outbox append path를 추가해 `branch.updated` event가 pending/published 대상이 되게 한다.
- [ ] `ScanAgentRunRevision` 또는 그 adapter result가 matched와 published를 구분해야 하면 result DTO와 worker payload mapping을 함께 갱신한다.
- [ ] fake broadcaster 또는 fake durable event store 테스트로 policy-allowed push 이후 branch.updated event가 실제로 publish/store되는지 검증한다.
- [ ] publish/store 실패 시 operation failed와 sanitized payload가 남고 `revision_event_matched=false`가 되는 회귀 테스트를 추가한다.
- [ ] `cd services/core && go test -count=1 ./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`를 실행한다.
- [ ] `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_API-1] Publish branch event from worker revision scans

#### 문제

[services/core/cmd/worker/main.go](/config/workspace/gito/services/core/cmd/worker/main.go:58)는 `controlplane.NewRuntimeWithStore(nil, pgStore)`를 scanner로 주입한다. 이 runtime은 broadcaster가 없기 때문에 [services/core/internal/controlplane/runtime.go](/config/workspace/gito/services/core/internal/controlplane/runtime.go:238)의 `BroadcastEnvelope`가 호출되지 않는다. 그런데 [packages/contracts/notes/gito-control-plane.md](/config/workspace/gito/packages/contracts/notes/gito-control-plane.md:111)는 `revision_event_matched`를 generated/published event evidence로 설명하고, [agent-contract/provided/gito-forgejo-branch-events-v1.md](/config/workspace/gito/agent-contract/provided/gito-forgejo-branch-events-v1.md:190)는 proto-socket broadcast를 consumer path로 둔다.

#### 해결 방법

worker scan success는 "revision changed"와 "event published/stored"를 모두 만족할 때만 completion payload에 `revision_event_matched=true`를 기록한다. 현재 architecture에서 즉시 proto-socket broadcast가 불가능하면 durable outbox boundary를 사용해 consumer-visible event로 남기고, 그렇지 못하면 operation failed로 처리한다.

권장 구현 shape:

```go
// before: worker scanner runtime has no publishing boundary.
scanner = controlplane.NewRuntimeWithStore(nil, pgStore)
```

```go
// after shape: inject a real publishing boundary.
publisher := controlplane.NewOutboxBroadcaster(pgStore.OperationEvents())
scanner = controlplane.NewRuntimeWithStore(publisher, pgStore)
```

`NewOutboxBroadcaster`가 필요하다면 `protosocket.Envelope`의 `action=branch.updated` payload를 `events.Event{Type: events.BranchUpdated, Subject: "repo:<repo_id>"}`로 변환해 `operation_events` pending outbox에 append한다. `operation_events.operation_id`는 migration상 nullable이므로, branch/repo events에 empty operation id를 허용할지 새 generic event store seam을 둘지 코드와 테스트로 명확히 고정한다.

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

- [ ] `services/core/internal/controlplane/runtime.go`: `ScanAgentRunRevision` result가 published boundary를 반영하도록 보완한다.
- [ ] `services/core/cmd/worker/main.go`: scanner runtime에 non-nil publishing boundary를 주입한다.
- [ ] `services/core/internal/storage/storage.go` / `services/core/internal/storage/postgres.go`: durable outbox path가 필요하면 branch.updated append를 지원한다.
- [ ] `services/core/internal/controlplane/runtime_test.go`: nil broadcaster와 real/fake broadcaster 결과 차이를 테스트한다.
- [ ] `services/core/internal/worker/runner_test.go`: publish failure가 operation failed 및 `revision_event_matched=false`로 이어지는지 테스트한다.
- [ ] `packages/contracts/notes/gito-control-plane.md` 또는 provided contract: payload semantics가 바뀌면 transport-independent wording만 갱신한다.

#### 테스트 작성

작성한다. 최소 테스트:

- fake broadcaster/store가 있는 scanner에서 push 후 `branch.updated` event가 한 번 publish/store되고 worker payload에 `revision_event_matched=true`, `revision_event_id`가 들어간다.
- nil/failing publisher에서 scan matched만으로는 success가 되지 않고 operation failed, `revision_event_matched=false`, sanitized message가 남는다.
- existing Forgejo webhook branch event tests가 깨지지 않는다.

#### 중간 검증

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

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

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/cmd/worker/main.go` | REVIEW_REVIEW_API-1 |
| `services/core/internal/controlplane/runtime.go` | REVIEW_REVIEW_API-1 |
| `services/core/internal/controlplane/runtime_test.go` | REVIEW_REVIEW_API-1 |
| `services/core/internal/worker/runner.go` | REVIEW_REVIEW_API-1 |
| `services/core/internal/worker/runner_test.go` | REVIEW_REVIEW_API-1 |
| `services/core/internal/storage/storage.go` | REVIEW_REVIEW_API-1, outbox support 필요 시 |
| `services/core/internal/storage/postgres.go` | REVIEW_REVIEW_API-1, outbox support 필요 시 |
| `services/core/internal/storage/postgres_test.go` | REVIEW_REVIEW_API-1, outbox support 필요 시 |
| `packages/contracts/notes/gito-control-plane.md` | REVIEW_REVIEW_API-1, semantics 변경 시 |
| `agent-contract/provided/gito-forgejo-branch-events-v1.md` | REVIEW_REVIEW_API-1, provided contract 변경 시 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./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
```

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

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