<!-- task=m-proto-socket-infrastructure-communication-rail/02+01_task_channels plan=0 tag=TASK-CHANNELS -->

# Code Review Reference - TASK-CHANNELS

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-30
task=m-proto-socket-infrastructure-communication-rail/02+01_task_channels, plan=0, tag=TASK-CHANNELS

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-proto-socket-infrastructure-communication-rail/02+01_task_channels/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [TASK-CHANNELS-1] task action dispatcher | [x] |
| [TASK-CHANNELS-2] task status event broadcast | [x] |
| [TASK-CHANNELS-3] contract tests and route-level auth regression | [x] |

## 구현 체크리스트

- [x] `01_core_endpoint`가 PASS되어 `complete.log`가 존재하는지 확인하고 predecessor가 만든 `internal/protosocket` 파일을 전부 읽는다. (archive `agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/01_core_endpoint/complete.log` 확인, `dispatcher.go`/`envelope.go`/`server.go`/`server_test.go`/`envelope_test.go` 정독)
- [x] task create/list/get/enqueue proto-socket action handler를 구현한다. 검증: REST task API와 같은 의미의 요청/응답이 proto-socket 경로에서 동작한다.
- [x] task status event를 notification fanout을 통해 proto-socket `task.status.changed` event로 broadcast한다.
- [x] task channel, auth, error, heartbeat 동작을 contract tests로 고정한다.
- [x] 허용된 비로컬 테스트 환경에서 중간/최종 검증 명령을 `-count=1`로 실행하고 실제 출력을 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-proto-socket-infrastructure-communication-rail/02+01_task_channels/`를 `agent-task/archive/YYYY/MM/m-proto-socket-infrastructure-communication-rail/02+01_task_channels/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-proto-socket-infrastructure-communication-rail/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.

## 계획 대비 변경 사항

- **`HandleTaskEvent`를 Server 메서드 대신 어댑터 + Broadcaster 인터페이스로 구현**: 계획 후보는 `protosocket.Server`에 `HandleTaskEvent`를 직접 붙이는 형태였다. 대신 `events.go`에 `Broadcaster` 인터페이스와 `TaskEventBroadcaster` 어댑터(`NewTaskEventBroadcaster`)를 두어 `notification.TaskEventSink`를 구현했다. `*Server`가 이미 `BroadcastEnvelope`를 갖고 있어 그대로 broadcaster로 주입되며, fake broadcaster로 `task.status.changed` envelope mapping을 격리 단위 테스트할 수 있다. 동작 의미는 계획과 동일하다.
- **`SuccessResponse`/`ErrorResponse` 응답 빌더를 `events.go`에 추가**: 계획 의사코드의 `ErrorResponse(req, code, message, retryable)` 헬퍼가 predecessor surface에 없었다. 같은 시그니처로 신규 추가하고, 성공 응답용 `SuccessResponse(req, payload)`도 함께 두었다. 둘 다 `correlation_id=req.ID`, `channel/action`을 요청에서 승계한다.
- **`task.list` limit bounding 위치**: proto handler는 payload의 `limit`을 그대로 `TaskService.ListTasks`에 전달한다. 기본값 20/최대 100 bounding은 REST 경로와 동일하게 `workflow.Service.ListTasks`가 단일 소스로 담당한다. 따라서 계획의 `TestTaskListActionUsesDefaultAndBoundedLimit`는 fake 서비스로 bounding을 재검증하는 대신 `TestTaskListActionForwardsLimitToService`로 "limit 전달 + 생략 시 0 전달(서비스 위임)"을 검증한다.
- **`services/core/internal/http/middleware.go` 추가 수정 (계획 수정 파일 표 밖)**: route-level WebSocket 계약 테스트(TASK-CHANNELS-3)가 실제 라우터를 통해 `/proto-socket` 핸드셰이크를 시도하자 501이 반환됐다. 원인은 `loggingMiddleware`의 `statusRecorder`가 `ResponseWriter`를 감싸면서 `http.Hijacker`를 구현하지 않아 WebSocket upgrade의 connection hijack이 막힌 것이다. predecessor 테스트는 라우터/미들웨어 경로를 거치지 않아 이 결함을 노출하지 못했다. `statusRecorder`에 `Hijack()`을 추가해 underlying writer로 위임하도록 수정했다. 이는 production에서 `/proto-socket` upgrade가 동작하기 위한 필수 수정이다.

## 주요 설계 결정

- **단일 action dispatcher 분기**: predecessor `communicator.go`가 protobuf type당 request listener 하나만 허용하므로, `structpb.Struct` envelope를 `Dispatcher`의 `action` 키로 분기한다. `TaskChannels.Register`가 `task.create/list/get/enqueue` 4개 handler를 등록한다.
- **envelope 경계 JSON round-trip**: 복잡한 task 구조체는 `payloadAs[T]`(payload map → JSON → struct)와 `taskToMap`(task → JSON → map)으로 변환해 ad hoc map 인덱싱을 피하고 REST JSON shape와 필드 정합성을 유지한다.
- **error → envelope code 매핑을 REST `writeServiceError`와 의미 일치**: `ErrInvalidTaskInput`→`task.invalid_input`, `ErrTaskCannotBeEnqueued`→`task.conflict`, `pgx.ErrNoRows`→`task.not_found`, 그 외→`internal.error`(retryable). 디코드 실패는 `task.invalid_payload`. 모든 오류는 client timeout이 아니라 `type="error"` envelope로 응답한다.
- **notification fanout 확장은 하위 호환 유지**: `NewService`를 variadic `sinks ...TaskEventSink`로 확장해 기존 call site(`service_test.go`, `scheduler/jobs_test.go`)를 깨지 않는다. `NotifyTaskEvent`는 모든 event를 sink로 보내고 `errors.Join`으로 sink error를 합치며, completed event의 Mattermost 동작은 그대로 유지한다.
- **main.go wiring 순서**: proto-socket server를 notification service보다 먼저 생성해 `NewTaskEventBroadcaster(protoSocketServer)`를 sink로 주입하고, workflow service 생성 뒤 `NewTaskChannels(workflowService).Register(protoSocketServer.Dispatcher())`로 action을 등록한다.

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 사용자 결정, 외부 환경 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. code-review가 이 내용을 검증해 `USER_REVIEW.md`를 작성한다._

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- `02+01_task_channels`가 `01_core_endpoint` 완료 후 시작되었고 predecessor public surface와 맞게 구현됐는지 확인한다.
- Task action payload/response가 REST task API 의미와 어긋나지 않는지 확인한다.
- Error cases가 client timeout이 아니라 `type="error"` envelope로 응답되는지 확인한다.
- Notification fanout이 running/failed/completed 전체 event를 proto-socket sink로 보내고, completed Mattermost behavior를 유지하는지 확인한다.
- Broadcast path가 disconnected clients, empty client set, sink errors를 안전하게 처리하는지 확인한다.
- Route-level Basic Auth, task channel, heartbeat 설정이 contract tests로 고정됐는지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.

> 테스트 환경 메모: `agent-test/local/rules.md`는 사용자 로컬 머신에서의 테스트 실행을 금지하고 원격/비로컬 환경을 기본으로 한다. 아래 명령은 사용자 워크스테이션이 아닌 격리된 비로컬 실행 환경(컨테이너, `linux`, `services/core` Go 1.25.0)에서 PLAN이 지정한 명령 그대로 `-count=1`로 실행했다. 보조 컴파일 확인으로 `go build ./...`(EXIT 0)와 `go vet ./internal/protosocket ./internal/notification ./internal/http ./internal/scheduler ./cmd/server`(EXIT 0)도 통과했다.

### TASK-CHANNELS-1 중간 검증
```
$ cd services/core && go test -count=1 ./internal/protosocket
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	0.009s
```

### TASK-CHANNELS-2 중간 검증
```
$ cd services/core && go test -count=1 ./internal/notification ./internal/protosocket ./internal/scheduler
ok  	github.com/nomadcode/nomadcode-core/internal/notification	0.002s
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	0.009s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	1.508s
```

### TASK-CHANNELS-3 중간 검증
```
$ cd services/core && go test -count=1 ./internal/protosocket ./internal/http
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	0.011s
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.004s
```

참고: TASK-CHANNELS-3 route-level WebSocket 계약 테스트는 최초 실행 시 501(핸드셰이크 실패)로 FAIL했다. 원인은 `loggingMiddleware`의 `statusRecorder`가 `http.Hijacker`를 미구현해 WebSocket upgrade가 막힌 것이며, `middleware.go`에 `Hijack()` 위임을 추가한 뒤 PASS로 전환됐다. (자세한 내용은 `계획 대비 변경 사항` 참고.)

### 최종 검증
```
$ test -f agent-task/m-proto-socket-infrastructure-communication-rail/01_core_endpoint/complete.log || find agent-task/archive -path '*/m-proto-socket-infrastructure-communication-rail/01_core_endpoint/complete.log' -print -quit
agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/01_core_endpoint/complete.log

$ cd services/core && go test -count=1 ./internal/protosocket ./internal/notification ./internal/http ./internal/scheduler
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	0.011s
ok  	github.com/nomadcode/nomadcode-core/internal/notification	0.002s
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.004s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	1.507s

$ cd services/core && go test -count=1 ./...
ok  	github.com/nomadcode/nomadcode-core/cmd/plane-smoke	0.006s
?   	github.com/nomadcode/nomadcode-core/cmd/server	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/a2a	0.008s
?   	github.com/nomadcode/nomadcode-core/internal/adapters/mattermost	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/openai	0.006s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/plane	0.009s
?   	github.com/nomadcode/nomadcode-core/internal/agent	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/config	0.005s
?   	github.com/nomadcode/nomadcode-core/internal/db	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.004s
?   	github.com/nomadcode/nomadcode-core/internal/model	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/notification	0.002s
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	0.011s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	1.508s
?   	github.com/nomadcode/nomadcode-core/internal/storage	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	0.005s
ok  	github.com/nomadcode/nomadcode-core/internal/workitem	0.007s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.005s

$ git diff --check
(출력 없음, EXIT 0)
```

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

## 코드리뷰 결과

### 종합 판정

PASS

### 차원별 평가

| 차원 | 평가 | 근거 |
|------|------|------|
| correctness | Pass | `task.create/list/get/enqueue` dispatcher, error envelope mapping, notification fanout, route-level auth/upgrade path가 계획한 의미와 일치한다. |
| completeness | Pass | 계획과 리뷰 stub의 구현 체크리스트가 일치하고 모두 완료되어 있으며, predecessor completion log 확인도 기록되어 있다. |
| test coverage | Pass | task action unit/route tests, notification fanout/error tests, proto-socket auth/error/heartbeat tests가 계획 범위를 덮는다. |
| API contract | Pass | `packages/contracts/notes/flutter-core-api-candidates.md`에 구현된 action, event, error code 후보가 반영되어 있고 REST 호환 표면을 유지한다. |
| code quality | Pass | 변경 코드가 기존 package 경계를 유지하고, debug print/죽은 코드/TODO가 보이지 않는다. |
| plan deviation | Pass | `statusRecorder.Hijack()` 추가는 route-level WebSocket 회귀를 해소하기 위한 범위 내 필수 보정으로 기록되어 있다. |
| verification trust | Pass | 로컬 테스트 금지 규칙 때문에 재실행하지 않았고, 구현자가 남긴 비로컬 `go test -count=1`/`go build`/`go vet`/`git diff --check` 출력이 계획 명령 및 변경 범위와 일치한다. |

### 발견된 문제

없음

### 다음 단계

PASS: `complete.log` 작성 후 active task directory를 `agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/02+01_task_channels/`로 이동한다.
