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

# Workflow Core Notification Event Model Follow-up Plan

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

이 계획은 `code_review_cloud_G07_0.log`의 Required 이슈만 해결한다. 구현 완료는 active `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션과 검증 출력 작성까지다. 사용자 결정이나 외부 환경 준비가 필요한 blocker만 `사용자 리뷰 요청`으로 기록한다.

## 배경

첫 리뷰에서 `notification.TaskEvent.Reason`이 `task.Metadata.status_reason`을 event type과 무관하게 복사하는 문제가 발견됐다. 실패 후 재시도되는 task는 기존 실패 사유가 metadata에 남아 있을 수 있고, 새 attempt의 running/completed `task.status.changed` event가 이전 실패 사유를 다시 broadcast할 수 있다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 USER_REVIEW 작성 여부를 판단한다.

## Roadmap Targets

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

## 분석 결과

### 읽은 파일

- `agent-task/m-workflow-core/05+02,03,04_notification_model/plan_cloud_G07_0.log`
- `agent-task/m-workflow-core/05+02,03,04_notification_model/code_review_cloud_G07_0.log`
- `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`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/internal/workflow/lifecycle.go`
- `services/core/internal/protosocket/events.go`
- `services/core/internal/protosocket/tasks_test.go`
- `packages/contracts/notes/flutter-core-api-candidates.md`

### 테스트 환경 규칙

- test_env: `local`
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/core-smoke.md`, `agent-test/local/contracts-smoke.md`
- 적용 명령: `cd services/core && go test ./...`
- contracts note는 이미 additive shape를 담고 있으므로 이번 follow-up은 code/test 중심이다.

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

- `TestWorkRetriesAfterFailureState`는 재시도 성공 자체만 확인하고 notification event payload는 확인하지 않는다.
- `TestWorkEmitsRunningAndCompletedEvents`와 `TestWorkEmitsFailedEvent`는 단일 attempt만 확인해 stale reason regression을 잡지 못한다.

### 심볼 참조

- Required finding: [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:395)
- Related lifecycle behavior: [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:172), [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:216)
- Event mapper: [services/core/internal/protosocket/events.go](/config/workspace/nomadcode/services/core/internal/protosocket/events.go:61)

### 범위 결정 근거

- 후속 범위는 stale `reason` 방지와 regression test에 한정한다.
- 기존 additive `TaskEvent` fields, `attempt`, `occurred_at`, contracts note shape는 유지한다.
- Mattermost completed notification behavior는 변경하지 않는다.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`
- 근거: proto-socket event payload 정확성과 scheduler retry path가 얽힌 cross-boundary correctness follow-up이다.

## 구현 체크리스트

- [ ] [REVIEW_WFC-EVENTS-1] `task.status.changed` reason이 재시도 성공 경로에서 stale `status_reason`을 노출하지 않도록 event context 매핑과 회귀 테스트를 보강한다.
- [ ] 중간 및 최종 검증 명령을 실행하고 출력 원문을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_WFC-EVENTS-1] Stale Reason 방지

### 문제

- [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:395)는 metadata의 `status_reason`을 event type 구분 없이 `Reason`으로 복사한다.
- [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:172)는 새 attempt 시작 시 기존 `status_reason`을 지우지 않는다.
- [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:216)는 result에 `summary`가 없으면 completed metadata의 `status_reason`을 갱신하지 않는다.
- 따라서 실패 후 재시도 성공 시 running/completed event가 이전 failed reason을 포함할 수 있다.

### 해결 방법

가장 작은 변경으로 stale reason을 막는다.

- scheduler event context helper가 event type 또는 target status를 입력받아 `Reason`을 싣는 조건을 명확히 한다.
- 최소 기준: failed event는 failure reason을 유지하고, running event는 이전 failure reason을 내보내지 않는다.
- completed event는 현재 completion에서 생성된 summary라고 보장할 수 있을 때만 reason을 내보내거나, 보장이 어렵다면 빈 reason으로 둔다.
- `TaskStatusChangedEnvelope`가 optional field 의미와 맞도록 empty `reason`은 payload에서 생략하는 것도 허용된다.

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

- [ ] `services/core/internal/scheduler/jobs.go`: stale `status_reason`이 running/completed event로 복사되지 않게 event context 매핑을 조정한다.
- [ ] `services/core/internal/scheduler/jobs_test.go`: 실패 후 재시도 성공 시 emitted events에서 second running/completed reason이 비어 있거나 생략되는지 검증한다. failed event reason은 유지한다.
- [ ] 필요 시 `services/core/internal/protosocket/events.go`와 `services/core/internal/protosocket/tasks_test.go`: optional `reason` omission을 코드와 테스트에 반영한다.

### 테스트 작성

- Required:
  - `TestWorkRetriesAfterFailureState`를 확장하거나 새 test를 추가해 notification sink event sequence를 검증한다.
  - 기대 event 예시: first running reason empty, failed reason `"first attempt agent failure"`, second running reason empty, completed reason empty.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/scheduler ./internal/protosocket
```

Expected: scheduler retry event regression과 proto-socket payload tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/scheduler/jobs.go` | REVIEW_WFC-EVENTS-1 |
| `services/core/internal/scheduler/jobs_test.go` | REVIEW_WFC-EVENTS-1 |
| `services/core/internal/protosocket/events.go` | REVIEW_WFC-EVENTS-1, optional field cleanup if needed |
| `services/core/internal/protosocket/tasks_test.go` | REVIEW_WFC-EVENTS-1, optional field cleanup if needed |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/scheduler ./internal/protosocket ./internal/notification
rg --sort path -n 'reason|status_reason|task.status.changed' services/core/internal/scheduler services/core/internal/protosocket packages/contracts/notes/flutter-core-api-candidates.md
cd services/core && go test ./...
```

Expected: focused tests pass, deterministic `rg` shows no stale reason mapping risk, full core tests pass.

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