<!-- task=m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode plan=0 tag=API -->

# Plan - API

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수입니다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고하세요. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용입니다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 scope conflict가 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채우고 멈추세요. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않으며, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않습니다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰가 아닙니다.

## 배경

현재 NomadCode Gito consumer의 production transport는 `nhooyr.io/websocket` text frame에 JSON envelope을 싣습니다. Gito 계약은 `/proto-socket`이 binary protobuf `PacketBase` frame과 `google.protobuf.Struct` payload만 지원한다고 명시하므로, 실제 wire와 맞지 않습니다. 이 계획은 transport 교체와 binary frame에서 온 `structpb.Struct`를 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환하는 작업을 먼저 닫습니다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록합니다. 구현 에이전트의 직접 사용자 질문은 금지되며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정합니다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
  - `transport-adapter`: JSON text websocket transport를 실제 proto-socket binary/protobuf transport 또는 동등 adapter로 교체
  - `envelope-decode`: binary frame에서 받은 `structpb.Struct` envelope을 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- `/config/workspace/gito/agent-contract/index.md`
- `/config/workspace/gito/agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `services/core/internal/gitosync/runner.go`
- `services/core/internal/gitoevents/client.go`
- `services/core/internal/gitoevents/events.go`
- `services/core/internal/gitoevents/client_test.go`
- `services/core/internal/gitoevents/events_test.go`
- `services/core/internal/protosocket/envelope.go`
- `services/core/internal/protosocket/envelope_test.go`
- `services/core/internal/protosocket/server.go`
- `services/core/internal/protosocket/server_test.go`
- `services/core/internal/protosocket/tasks_test.go`
- `services/core/go.mod`
- `/config/go/pkg/mod/git.toki-labs.com/toki/proto-socket/go@v0.0.0-20260530100557-fed0da37fa1a/ws_client.go`
- `/config/go/pkg/mod/git.toki-labs.com/toki/proto-socket/go@v0.0.0-20260530100557-fed0da37fa1a/communicator.go`

### 테스트 환경 규칙

`test_env=local`입니다. `agent-test/local/rules.md`와 `agent-test/local/core-smoke.md`, roadmap/doc touch 때문에 `agent-test/local/workspace-ops-smoke.md`를 읽었습니다. 적용 명령은 core 변경 최소 `cd services/core && go test ./...`와 문서/roadmap preflight `git diff --check`입니다. remote runner profile은 standard runner를 기준으로 하지만, 구현 task의 deterministic contract는 현재 checkout에서 같은 Go test와 diff check를 실행하는 것으로 둡니다.

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

`gitosync/runner.go`의 text JSON transport는 현재 직접 테스트가 없습니다. `protosocket/server_test.go`와 `tasks_test.go`는 binary proto-socket client/server 사용 예시를 검증하지만 Gito consumer transport에는 적용되지 않습니다. 이 계획에서 production transport 단위/route 테스트를 추가해야 합니다.

### 심볼 참조

none. 기존 exported symbol rename/remove 없이 `websocketTransport` 내부 구현 또는 새 unexported transport helper만 바꿉니다.

### 분할 판단

split decision policy를 먼저 평가했습니다. Wire transport와 binary struct decode는 같은 protocol boundary이며 함께 리뷰해야 합니다. `bridge-e2e`와 `failure-ops`는 이 작업의 complete.log 뒤에 검증하는 downstream behavior라서 별도 dependent subtasks로 분리했습니다. 공유 task group은 `m-gito-protosocket-consumer-wire-readiness`이고, 이 plan은 `01_wire_transport_decode`로 선행 의존성이 없습니다.

### 범위 결정 근거

이 plan은 `services/core/internal/gitosync/runner.go`, 필요한 경우 `services/core/internal/gitoevents/*`, 해당 tests만 수정합니다. branch scan/enqueue 의미, config/env wiring, Plane job semantics, Gito producer 구현은 제외합니다.

### 빌드 등급

`cloud-G07`입니다. 실제 binary protobuf wire, async listener/request 흐름, malformed frame handling이 protocol/schema risk를 갖고 기존 JSON path를 교체합니다.

## 구현 체크리스트

- [ ] `gitosync` production transport가 `git.toki-labs.com/toki/proto-socket/go` `WsClient`와 `protosocket.ParserMap()`으로 binary `structpb.Struct` frame을 송수신하도록 바꾼다.
- [ ] `structpb.Struct` 수신값을 `protosocket.EnvelopeFromStruct`로 변환하고 malformed struct, non-Struct frame, wrong channel/action, payload 누락을 side effect 없이 drop 또는 error path로 처리하는 테스트를 추가한다.
- [ ] 실제 proto-socket test server가 `event.subscribe`를 받고 `branch.updated`를 binary frame으로 돌려주는 focused test를 추가한다.
- [ ] `git diff --check`와 `cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Replace JSON websocket transport with binary proto-socket transport

문제: [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:32)의 `websocketTransport` 설명과 [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:49)의 `Write`, [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:60)의 `Read`가 JSON text frame을 사용합니다. Gito contract는 JSON text frame 미지원, binary protobuf `PacketBase` + `structpb.Struct`만 지원합니다.

해결 방법: `websocket.Dial` 이후 `toki.NewWsClient(conn, 0, 0, protosocket.ParserMap())` 또는 configurable heartbeat helper를 사용합니다. `Write`는 `env.ToStruct()` 후 `client.Communicator.Send(structMsg)`로 binary frame을 보내고, `Read`는 `toki.AddListenerTyped[*structpb.Struct]` callback이 `EnvelopeFromStruct` 결과를 channel에 넣도록 구현합니다. `Close`는 `WsClient.Close()`와 websocket close를 idempotent하게 처리합니다.

Before:

```go
// services/core/internal/gitosync/runner.go:49
func (t *websocketTransport) Write(ctx context.Context, env protosocket.Envelope) error {
	if t.conn == nil {
		return errors.New("gitosync: transport not connected")
	}
	data, err := json.Marshal(env)
	if err != nil {
		return err
	}
	return t.conn.Write(ctx, websocket.MessageText, data)
}
```

After direction:

```go
func (t *protoSocketTransport) Write(ctx context.Context, env protosocket.Envelope) error {
	if t.client == nil {
		return errors.New("gitosync: transport not connected")
	}
	msg, err := env.ToStruct()
	if err != nil {
		return err
	}
	return t.client.Communicator.Send(msg)
}
```

수정 파일 및 체크리스트:

- [ ] `services/core/internal/gitosync/runner.go`에서 JSON marshal/unmarshal transport를 제거하고 binary transport를 추가한다.
- [ ] `encoding/json` import가 더 이상 필요 없으면 제거한다.
- [ ] `google.golang.org/protobuf/types/known/structpb` import가 필요하면 추가한다.

테스트 작성: `services/core/internal/gitosync/runner_test.go`를 새로 만들거나 기존 test 파일에 transport-focused tests를 추가합니다. test server는 `protosocket.NewServer` 또는 local `httptest` + `toki.NewWsClient`를 사용합니다.

중간 검증:

```bash
cd services/core && go test ./internal/gitosync ./internal/protosocket
```

### [API-2] Convert inbound Struct envelopes through the shared envelope boundary

문제: [protosocket/envelope.go](/config/workspace/nomadcode/services/core/internal/protosocket/envelope.go:67)에 이미 `EnvelopeFromStruct`가 있고 [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:68)는 JSON unmarshal로 `Envelope`을 만듭니다. binary path에서 decode boundary를 공유하지 않으면 `structpb.Struct` malformed handling과 event decode가 갈라집니다.

해결 방법: inbound `*structpb.Struct` listener callback에서 `protosocket.EnvelopeFromStruct`를 호출하고 성공한 envelope만 `Read` queue에 넣습니다. nil struct 또는 malformed payload는 log/error queue로 처리하되 `gitoevents.Client.handleEnvelope`에 partial envelope을 전달하지 않습니다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/gitosync/runner.go`에 inbound channel과 decode error path를 둔다.
- [ ] `Read(ctx)`는 context cancel, transport close, decode error를 구분해 반환한다.
- [ ] wrong channel/action은 transport가 아니라 `gitoevents.Client`의 기존 drop path에 맡긴다.

테스트 작성: malformed `structpb.Struct`, wrong action, missing payload frame을 보내 handler가 호출되지 않는 테스트를 추가합니다.

중간 검증:

```bash
cd services/core && go test ./internal/gitoevents ./internal/gitosync
```

### [API-3] Prove `event.subscribe` request and `branch.updated` binary event interop

문제: [gitoevents/client.go](/config/workspace/nomadcode/services/core/internal/gitoevents/client.go:89)는 subscribe envelope을 보내지만 현재 production transport test는 fake transport뿐입니다. 실제 proto-socket test server가 binary request를 받는 검증이 없습니다.

해결 방법: test server가 `toki.AddRequestListenerTyped[*structpb.Struct,*structpb.Struct]`로 `event.subscribe`를 받고, subscription response 후 `branch.updated` event를 `WsClient.Send` 또는 server broadcast로 전송합니다. `gitoevents.Client` handler가 정확히 한 번 호출되는지 검증합니다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/gitosync/runner_test.go`에 binary subscribe/event test를 추가한다.
- [ ] request envelope의 `channel=event`, `action=event.subscribe`, payload `events=["branch.updated"]`, `repo_id`, `branch`를 assert한다.
- [ ] broadcast event는 Gito contract base payload를 사용한다.

테스트 작성: 필수. 테스트명 예: `TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent`.

중간 검증:

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

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `services/core/internal/gitosync/runner.go` | API-1, API-2 |
| `services/core/internal/gitosync/runner_test.go` | API-1, API-2, API-3 |
| `services/core/internal/gitoevents/events_test.go` | API-2 if extra decode regression is needed |

## 최종 검증

```bash
git diff --check
cd services/core && go test ./...
```

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