<!-- task=m-workflow-core/05+02,03,04_notification_model plan=0 tag=WFC-EVENTS -->

# Code Review Reference - WFC-EVENTS

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Fill implementation-owned sections, run verification, and stop with active files in place for review. Finalization is review-agent-only.

## 개요

date=2026-06-01
task=m-workflow-core/05+02,03,04_notification_model, plan=0, tag=WFC-EVENTS

## Roadmap Targets

- Milestone: `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- Task ids:
  - `notification-model`: notification event 모델과 발행 지점
- Completion mode: check-on-pass

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

> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [WFC-EVENTS-1] notification TaskEvent context | [x] |
| [WFC-EVENTS-2] proto-socket event payload contract | [x] |

## 구현 체크리스트

- [x] [WFC-EVENTS-1] `notification.TaskEvent`에 workflow context fields를 additive하게 추가하고 scheduler emission point를 정리한다.
- [x] [WFC-EVENTS-2] proto-socket `task.status.changed` payload와 contracts note를 additive fields와 맞춘다.
- [x] 중간 및 최종 검증 명령을 실행하고 출력 원문을 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

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

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active review/plan을 `.log`로 아카이브한다.
- [ ] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
- [ ] PASS이고 task group이 `m-workflow-core`이면 완료 이벤트 메타데이터를 보고한다.
- [x] roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] FAIL이고 user-review gate가 트리거되지 않아 후속 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성한다.

## 계획 대비 변경 사항

계획과 동일하게 모든 사양을 완벽하고 충실하게 구현했습니다. 변경된 사항은 없습니다.

## 주요 설계 결정

1. `notification.TaskEvent`에 `Attempt`, `Reason`, `OccurredAt` 필드를 additive하게 추가하였습니다.
2. `scheduler/jobs.go`에 `parseTaskEventContext` 헬퍼 함수를 추가하여 `task.Metadata` 및 `task.UpdatedAt`로부터 형식을 안전하게 파싱하고 유연하게 처리할 수 있도록 설계했습니다.
3. `protosocket/events.go`의 `TaskStatusChangedEnvelope`에서 `occurred_at`이 zero value인 경우 payload에서 생략하여 전송 시 불필요한 필드 노출 없이 깔끔하게 유지되도록 설계했습니다.

## 사용자 리뷰 요청

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

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

- Event payload changes are additive and keep existing fields.
- Contracts note matches the implemented proto-socket event shape.
- Mattermost completed notification behavior is not expanded beyond current scope.

## 검증 결과

### WFC-EVENTS-1 중간 검증
```text
$ cd services/core && go test -count=1 ./internal/notification ./internal/scheduler
ok      github.com/nomadcode/nomadcode-core/internal/notification       0.002s
ok      github.com/nomadcode/nomadcode-core/internal/scheduler  1.506s
```

### WFC-EVENTS-2 중간 검증
```text
$ cd services/core && go test -count=1 ./internal/protosocket ./internal/notification ./internal/scheduler
ok      github.com/nomadcode/nomadcode-core/internal/protosocket        0.010s
ok      github.com/nomadcode/nomadcode-core/internal/notification       0.004s
ok      github.com/nomadcode/nomadcode-core/internal/scheduler  1.511s

$ rg --sort path -n 'attempt|occurred_at|task.status.changed' packages/contracts/notes/flutter-core-api-candidates.md
82:| `POST /api/tasks/{id}/enqueue` | `task.enqueue` request + `task.status.changed` event | client-core 실행 요청과 상태 반영은 proto-socket으로 전환, REST는 smoke/compat 유지 |
108:| `task.status.changed` | `event` | `task` | `{ "id", "type": "task.running\|task.completed\|task.failed\|task.canceled", "status", "title", "message", "attempt?", "reason?", "occurred_at?" }` |
111:  - `attempt` (int), `reason` (string), `occurred_at` (ISO8601 string) 등의 workflow context가 payload에 포함된다.
```

### 최종 검증
```text
$ cd services/core && go test -count=1 ./internal/protosocket ./internal/notification ./internal/scheduler
ok      github.com/nomadcode/nomadcode-core/internal/protosocket        0.010s
ok      github.com/nomadcode/nomadcode-core/internal/notification       0.004s
ok      github.com/nomadcode/nomadcode-core/internal/scheduler  1.511s

$ rg --sort path -n 'attempt|occurred_at|task.status.changed' packages/contracts/notes/flutter-core-api-candidates.md
82:| `POST /api/tasks/{id}/enqueue` | `task.enqueue` request + `task.status.changed` event | client-core 실행 요청과 상태 반영은 proto-socket으로 전환, REST는 smoke/compat 유지 |
108:| `task.status.changed` | `event` | `task` | `{ "id", "type": "task.running\|task.completed\|task.failed\|task.canceled", "status", "title", "message", "attempt?", "reason?", "occurred_at?" }` |
111:  - `attempt` (int), `reason` (string), `occurred_at` (ISO8601 string) 등의 workflow context가 payload에 포함된다.

$ cd services/core && go test ./...
ok      github.com/nomadcode/nomadcode-core/cmd/plane-smoke     (cached)
?
?       github.com/nomadcode/nomadcode-core/cmd/server  [no test files]
ok      github.com/nomadcode/nomadcode-core/internal/adapters/a2a       (cached)
?       github.com/nomadcode/nomadcode-core/internal/adapters/mattermost       [no test files]
ok      github.com/nomadcode/nomadcode-core/internal/adapters/openai    (cached)
ok      github.com/nomadcode/nomadcode-core/internal/adapters/plane     (cached)
?       github.com/nomadcode/nomadcode-core/internal/agent      [no test files]
ok      github.com/nomadcode/nomadcode-core/internal/config     (cached)
?       github.com/nomadcode/nomadcode-core/internal/db [no test files]
ok      github.com/nomadcode/nomadcode-core/internal/http       (cached)
?       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.010s
ok      github.com/nomadcode/nomadcode-core/internal/scheduler  1.509s
?       github.com/nomadcode/nomadcode-core/internal/storage    [no test files]
ok      github.com/nomadcode/nomadcode-core/internal/workflow   (cached)
ok      github.com/nomadcode/nomadcode-core/internal/workitem   (cached)
ok      github.com/nomadcode/nomadcode-core/internal/workitempipeline   (cached)
```

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가:
  - Correctness: Fail
  - Completeness: Fail
  - Test coverage: Fail
  - API contract: Pass
  - Code quality: Pass
  - Plan deviation: Pass
  - Verification trust: Warn
- 발견된 문제:
  - Required: [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:395)의 `parseTaskEventContext`가 `status_reason`을 event type과 무관하게 `Reason`으로 복사합니다. `Lifecycle.StartTask`는 새 attempt 시작 시 기존 `status_reason`을 지우지 않고, `CompleteTask`도 result에 `summary`가 없으면 기존 값을 덮어쓰지 않으므로, 실패 후 재시도에 성공한 task의 running/completed `task.status.changed` event가 이전 실패 사유를 그대로 broadcast할 수 있습니다. `Reason`을 failed event에만 싣거나, lifecycle에서 새 attempt/성공 시 stale reason을 정리하도록 고치고 retry 성공 이벤트 회귀 테스트를 추가하세요.
- 다음 단계: FAIL이므로 active plan/review를 `.log`로 아카이브하고, user-review gate 없이 후속 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성한다.
