<!-- task=workflow_core/01_lifecycle_foundation plan=0 tag=WORKFLOW -->

# Workflow Core Lifecycle Foundation

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증 명령을 실행하고 실제 출력, 구현 내용, 계획 대비 변경 사항을 채운 뒤 active 파일을 그대로 두고 리뷰 준비 상태를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review skill 전용이다.

## 배경

Work Item Provider Pipeline Design이 `pending -> queued -> running -> completed|failed|canceled` lifecycle 책임을 Workflow Core로 넘겼다. 현재 scheduler worker는 storage를 직접 호출해 상태를 바꾸므로 workflow service가 상태 전이 규칙을 소유하지 못한다. 이 작업은 lifecycle 상태 전이 API와 검증 테스트를 먼저 만들어 이후 retry/timeout과 notification event가 같은 상태 경계에 붙을 수 있게 한다.

## 분석 결과

### 읽은 파일

- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/workflow-core.md`
- `services/core/internal/workflow/model.go`
- `services/core/internal/workflow/service.go`
- `services/core/internal/workflow/service_test.go`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/internal/scheduler/river.go`
- `services/core/cmd/server/main.go`
- `services/core/internal/storage/store.go`
- `services/core/queries/tasks.sql`
- `services/core/internal/db/models.go`
- `services/core/go.mod`
- `services/core/bin/test`
- `bin/test`

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

- 상태 전이 검증: 기존 `workflow/service_test.go`는 metadata/external ref 정규화만 검증하고 lifecycle transition은 검증하지 않는다.
- scheduler 상태 전이: 기존 `scheduler/jobs_test.go`는 `runAgentTask`와 input builder만 검증하고 worker가 running/completed/failed를 어떤 API로 기록하는지 검증하지 않는다.
- storage SQL 자체는 기존 generated code를 사용하며 이 작업은 새 query를 요구하지 않는다.

### 심볼 참조

- renamed/removed symbols: none.
- 새로 도입할 call site: `scheduler.TaskWorker.Work`의 `Store.UpdateStatus`, `Store.CompleteTask`, `markFailed` storage 직접 호출을 workflow lifecycle interface 호출로 바꾼다.

### 분할 판단

split decision policy를 먼저 평가했다. shared task group은 `workflow_core`다.

- `01_lifecycle_foundation`: Workflow Core 상태 전이 API와 scheduler 연결. 의존 없음.
- `02+01_retry_timeout`: lifecycle API가 만든 metadata/transition 경계에 retry/timeout을 붙인다. `01_lifecycle_foundation`의 `complete.log` 필요.
- `03+01_notification_events`: lifecycle API의 상태 변화 결과를 event로 발행한다. `01_lifecycle_foundation`의 `complete.log` 필요.

이 작업은 lifecycle 기반만 소유하며 retry/timeout과 notification은 sibling plan으로 분리한다.

### 범위 결정 근거

- `services/core/internal/workitempipeline/**` idempotency 구현은 이 계획에서 제외한다. provider task 생성 중복 방지는 workflow 상태 전이와 다른 저장소/API 경계다.
- retry count, timeout duration, heartbeat metadata 변경은 `02+01_retry_timeout`에서 다룬다.
- notification event 모델과 Mattermost 연동 변경은 `03+01_notification_events`에서 다룬다.
- DB migration은 제외한다. 현재 상태 전이에는 기존 `tasks.status`, `result`, `error`, `metadata` 컬럼이면 충분하다.

### 빌드 등급

build=`cloud-G07`, review=`cloud-G07`: scheduler/workflow 상태 전이와 실패 경계가 바뀌며 테스트가 현재 부족하다.

## 구현 체크리스트

- [ ] [WORKFLOW-1] Workflow Core lifecycle API와 transition guard를 추가한다.
- [ ] [WORKFLOW-2] Scheduler worker가 storage 직접 상태 변경 대신 lifecycle API를 사용하게 한다.
- [ ] 중간 검증과 최종 검증을 모두 실행하고 실제 출력을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [WORKFLOW-1] Workflow Core lifecycle API와 transition guard를 추가한다

#### 문제

`services/core/internal/workflow/service.go:147`의 `EnqueueTask`만 상태 전이를 검증하고, running/completed/failed/canceled 전이는 workflow package에 없다. `services/core/internal/scheduler/jobs.go:44`, `55`, `294-302`는 storage를 직접 호출한다.

#### 해결 방법

`workflow` package에 lifecycle 전용 타입을 추가한다. 새 파일은 상태 전이 책임을 `Service`의 enqueue와 분리하고 scheduler 생성 순환 의존을 피하기 위한 것이다.

Before (`services/core/internal/scheduler/jobs.go:44`):

```go
task, err := w.Store.UpdateStatus(ctx, taskID, string(workflow.StatusRunning))
```

After:

```go
task, err := w.Lifecycle.StartTask(ctx, taskID)
```

추가할 API:

```go
type Lifecycle struct {
    store  *storage.Store
    logger *slog.Logger
}

func NewLifecycle(store *storage.Store, logger *slog.Logger) *Lifecycle
func (l *Lifecycle) StartTask(ctx context.Context, id string) (storage.Task, error)
func (l *Lifecycle) CompleteTask(ctx context.Context, id string, result json.RawMessage) (storage.Task, error)
func (l *Lifecycle) FailTask(ctx context.Context, id string, message string) (storage.Task, error)
func (l *Lifecycle) CancelTask(ctx context.Context, id string, message string) (storage.Task, error)
```

Transition guard:

- `pending|queued -> running`
- `running -> completed|failed|canceled`
- `failed -> queued` remains allowed through existing `EnqueueTask`
- invalid transitions return a new workflow error such as `ErrInvalidTaskTransition`

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

- [ ] `services/core/internal/workflow/model.go`: 필요 시 상태 전이 error 또는 metadata constants만 추가한다.
- [ ] `services/core/internal/workflow/service.go`: `ErrInvalidTaskTransition`와 shared transition helper를 추가하거나 새 `lifecycle.go`에서 사용한다.
- [ ] `services/core/internal/workflow/lifecycle.go`: lifecycle type과 methods를 추가한다.
- [ ] `services/core/internal/workflow/service_test.go`: transition success/failure tests를 추가한다.

#### 테스트 작성

작성한다.

- `TestLifecycleStartTaskAllowsQueuedAndMarksRunning`: fake/in-memory store가 없으므로 최소 단위는 transition helper table test로 작성한다.
- `TestCanTransitionRejectsInvalidLifecycleMove`: `completed -> running`, `pending -> completed`가 거부되는지 검증한다.

DB-backed store 통합 테스트는 현재 테스트 인프라가 없으므로 이 plan에서는 제외한다.

#### 중간 검증

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

기대 결과: workflow package tests pass. Go test cache output은 허용하지 않는다.

### [WORKFLOW-2] Scheduler worker가 storage 직접 상태 변경 대신 lifecycle API를 사용하게 한다

#### 문제

`TaskWorker`는 `services/core/internal/scheduler/jobs.go:31`에서 `Store *storage.Store`를 직접 들고, `jobs.go:44`, `55`, `294-302`에서 status/result/error를 직접 변경한다. 이 구조는 Workflow Core의 transition ownership과 충돌한다.

#### 해결 방법

`scheduler` package에 작은 interface를 두고 `workflow.Lifecycle`이 구현하게 한다.

Before (`services/core/internal/scheduler/jobs.go:28-35`):

```go
type TaskWorker struct {
    river.WorkerDefaults[TaskJobArgs]

    Store         *storage.Store
    Notifications *notification.Service
    Agent         agent.Client
    Model         model.Client
    Logger        *slog.Logger
}
```

After:

```go
type TaskLifecycle interface {
    StartTask(ctx context.Context, id string) (storage.Task, error)
    CompleteTask(ctx context.Context, id string, result json.RawMessage) (storage.Task, error)
    FailTask(ctx context.Context, id string, message string) (storage.Task, error)
}

type TaskWorker struct {
    river.WorkerDefaults[TaskJobArgs]

    Lifecycle     TaskLifecycle
    Notifications *notification.Service
    Agent         agent.Client
    Model         model.Client
    Logger        *slog.Logger
}
```

`scheduler.New`은 `*workflow.Lifecycle` 또는 `TaskLifecycle`을 받도록 바꾸고, `cmd/server/main.go:75-86`에서 `workflow.NewLifecycle(store, logger)`를 먼저 만든 뒤 scheduler와 workflow service에 공유한다.

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

- [ ] `services/core/internal/scheduler/jobs.go`: `TaskLifecycle` interface 추가, worker status mutation을 lifecycle 호출로 교체한다.
- [ ] `services/core/internal/scheduler/river.go`: `New` signature를 lifecycle 중심으로 조정한다.
- [ ] `services/core/cmd/server/main.go`: lifecycle 생성과 scheduler/workflow wiring을 조정한다.
- [ ] `services/core/internal/scheduler/jobs_test.go`: fake lifecycle을 추가해 success/fail path가 lifecycle method를 호출하는지 검증한다.

#### 테스트 작성

작성한다.

- `TestWorkCompletesThroughLifecycle`: fake lifecycle이 `StartTask`와 `CompleteTask` 호출을 기록하고 worker success path를 검증한다.
- `TestWorkFailsThroughLifecycle`: `runTask` error 시 `FailTask` 호출을 검증한다.

#### 중간 검증

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

기대 결과: scheduler package tests pass. Go test cache output은 허용하지 않는다.

## 의존 관계 및 구현 순서

1. `WORKFLOW-1` lifecycle API와 transition helper를 먼저 만든다.
2. `WORKFLOW-2` scheduler wiring을 lifecycle API로 옮긴다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/workflow/model.go` | WORKFLOW-1 |
| `services/core/internal/workflow/service.go` | WORKFLOW-1 |
| `services/core/internal/workflow/lifecycle.go` | WORKFLOW-1 |
| `services/core/internal/workflow/service_test.go` | WORKFLOW-1 |
| `services/core/internal/scheduler/jobs.go` | WORKFLOW-2 |
| `services/core/internal/scheduler/river.go` | WORKFLOW-2 |
| `services/core/internal/scheduler/jobs_test.go` | WORKFLOW-2 |
| `services/core/cmd/server/main.go` | WORKFLOW-2 |

## 최종 검증

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

기대 결과: all core Go tests pass. Go test cache output은 허용하지 않는다.

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