<!-- task=m-workflow-core/01_lifecycle_model plan=0 tag=WFC-LIFECYCLE -->

# Workflow Core Lifecycle Model Plan

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

이 계획은 구현 후 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채우는 것까지 포함한다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌이 필요하면 review stub의 `사용자 리뷰 요청` 섹션을 채우고 active 파일을 유지한 채 멈춘다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다.

## 배경

Workflow Core Milestone은 task lifecycle을 `enqueue -> running -> completed/failed`로 일관되게 만들고 이후 실패, retry, notification 작업이 같은 상태 모델을 공유하게 해야 한다. 현재 전이 규칙은 `workflow/lifecycle.go`의 `canTransition`과 `workflow/service.go`의 `canEnqueue`로 나뉘어 있어 후속 작업이 중복 판단을 늘릴 위험이 있다. 먼저 status/transition helper와 fake-store 기반 lifecycle 테스트 토대를 만든다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. code-review가 USER_REVIEW 작성 여부를 판단한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- Task ids:
  - `lifecycle-model`: task lifecycle 상태 모델 점검
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/workflow-core/PHASE.md`
- `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `services/core/internal/workflow/model.go`
- `services/core/internal/workflow/lifecycle.go`
- `services/core/internal/workflow/service.go`
- `services/core/internal/workflow/service_test.go`
- `services/core/internal/storage/store.go`
- `services/core/queries/tasks.sql`
- `services/core/internal/db/models.go`
- `services/core/internal/db/tasks.sql.go`
- `services/core/go.mod`
- `services/core/bin/test`

### 테스트 환경 규칙

- test_env: `local`
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/core-smoke.md`
- 적용 명령: core 변경은 `cd services/core && go test ./...`
- 보조 명령: 필요 시 `cd services/core && go test -count=1 ./internal/workflow`
- 주의: local 테스트 규칙은 기본 로컬 검증을 원격으로 한다고 적고 있으나, core-smoke 문서는 `services/core` Go test를 baseline으로 둔다. 구현 에이전트는 실행 환경에서 명령을 실제로 돌리고 출력 원문을 review stub에 기록한다.

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

- `canTransition`과 metadata merge helper는 현재 테스트가 있다.
- 실제 `Lifecycle.StartTask/CompleteTask/FailTask/CancelTask`는 concrete `*storage.Store` 의존 때문에 DB 없이 단위 테스트하기 어렵다.
- 이 작업은 store interface와 fake store 테스트를 추가해 상태 전이와 metadata 업데이트를 직접 검증한다.

### 심볼 참조

- renamed/removed symbols: none
- 참조 확인: `TaskStatus`, `StatusPending`, `StatusQueued`, `StatusRunning`, `StatusCompleted`, `StatusFailed`, `StatusCanceled`, `canTransition`, `canEnqueue`, `StartTask`, `CompleteTask`, `FailTask`, `CancelTask`를 `rg --sort path`로 확인했다.

### 분할 판단

- shared task group: `m-workflow-core`
- split output:
  - `01_lifecycle_model`: 상태 모델과 testability foundation. 선행 없음.
  - `02+01_transition_ownership`: 이 작업의 helper를 사용해 enqueue/worker 책임을 정리.
  - `03+01_failure_policy`: 이 작업의 fake-store 테스트 토대 위에서 failure metadata 검증.
  - `04+03_retry_timeout`: failure policy 이후 retry/timeout 정책 정리.
  - `05+02,03,04_notification_model`: transition/failure/retry 결과를 event payload에 연결.
- 이 계획은 첫 foundation이므로 predecessor completion 확인 대상이 없다.

### 범위 결정 근거

- DB schema는 기존 `status`, `error`, `metadata` 컬럼으로 충분하므로 migration/sqlc 변경은 제외한다.
- HTTP/proto-socket 응답 shape 변경은 후속 notification plan에서 다룬다.
- Plane live 상태 변경은 이미 별도 확인 완료했으며 이 작업에서 Plane API를 호출하지 않는다.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`
- 근거: workflow core 상태 모델은 scheduler, persistence, notification 후속 작업의 foundation이며 테스트 구조를 바꾸므로 broad internal API risk가 있다.

## 구현 체크리스트

- [ ] [WFC-LIFECYCLE-1] workflow lifecycle store interface와 status helper를 추가하고 기존 상태 전이 의미를 유지한다.
- [ ] [WFC-LIFECYCLE-2] fake store 기반 lifecycle 단위 테스트로 enqueue 이후 running/completed/failed 전이를 검증한다.
- [ ] 중간 및 최종 검증 명령을 실행하고 출력 원문을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [WFC-LIFECYCLE-1] Lifecycle Store Interface And Status Helpers

### 문제

- [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:13)는 concrete `*storage.Store`를 들고 있어 `Lifecycle` 메서드를 DB 없이 직접 검증하기 어렵다.
- [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:25)에는 lifecycle transition만 있고, [services/core/internal/workflow/service.go](/config/workspace/nomadcode/services/core/internal/workflow/service.go:175)의 enqueue 가능 여부는 별도 switch다.

### 해결 방법

Before [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:13):

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

After:

```go
type taskStore interface {
	CreateTask(context.Context, storage.CreateTaskInput) (storage.Task, error)
	GetTask(context.Context, string) (storage.Task, error)
	ListTasks(context.Context, int32) ([]storage.Task, error)
	UpdateMetadata(context.Context, string, json.RawMessage) (storage.Task, error)
	UpdateStatus(context.Context, string, string) (storage.Task, error)
	CompleteTask(context.Context, string, json.RawMessage) (storage.Task, error)
	FailTask(context.Context, string, string) (storage.Task, error)
}

type Lifecycle struct {
	store  taskStore
	logger *slog.Logger
}
```

Before [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:25):

```go
func canTransition(from, to TaskStatus) bool {
	switch from {
	case StatusPending, StatusQueued, StatusFailed:
		return to == StatusRunning
	case StatusRunning:
		return to == StatusCompleted || to == StatusFailed || to == StatusCanceled
	default:
		return false
	}
}
```

After:

```go
func validTaskStatus(status TaskStatus) bool { ... }
func terminalTaskStatus(status TaskStatus) bool { ... }
func canEnqueueStatus(status TaskStatus) bool { ... }
func canTransition(from, to TaskStatus) bool { ... } // same public behavior as today
```

Do not export these helpers unless a later package actually needs them.

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

- [ ] `services/core/internal/workflow/lifecycle.go`: introduce unexported `taskStore`; keep `NewLifecycle(store *storage.Store, ...)` signature for callers.
- [ ] `services/core/internal/workflow/lifecycle.go`: add `validTaskStatus`, `terminalTaskStatus`, `canEnqueueStatus`.
- [ ] `services/core/internal/workflow/service.go`: do not consume `canEnqueueStatus` yet unless trivial; transition ownership plan owns service behavior.

### 테스트 작성

- Add tests in `services/core/internal/workflow/service_test.go`.
- Test names:
  - `TestTaskStatusHelpersClassifyLifecycleStates`
  - `TestLifecycleStartCompleteFailTransitionsWithStore`

### 중간 검증

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

Expected: workflow package tests pass without DB.

## [WFC-LIFECYCLE-2] Lifecycle Transition Tests

### 문제

- [services/core/internal/workflow/service_test.go](/config/workspace/nomadcode/services/core/internal/workflow/service_test.go:75)는 `canTransition`만 테스트하고 실제 `StartTask`가 metadata then status를 쓰는 순서는 검증하지 않는다.

### 해결 방법

Add an unexported fake store in `service_test.go` that records `UpdateMetadata`, `UpdateStatus`, `CompleteTask`, and `FailTask` calls. Verify:

- pending or queued task can start and returns `running`.
- running task can complete and returns `completed`.
- running task can fail and returns `failed`.
- completed task cannot start.
- metadata gets `agent_run_state`, `last_heartbeat_at`, and incremented `attempt` on start.

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

- [ ] `services/core/internal/workflow/service_test.go`: add fake store type.
- [ ] `services/core/internal/workflow/service_test.go`: assert lifecycle method outputs and metadata keys.

### 테스트 작성

- Required, because this task changes testability and lifecycle helper semantics.

### 중간 검증

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

Expected: new lifecycle tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/workflow/lifecycle.go` | WFC-LIFECYCLE-1 |
| `services/core/internal/workflow/service.go` | WFC-LIFECYCLE-1 |
| `services/core/internal/workflow/service_test.go` | WFC-LIFECYCLE-1, WFC-LIFECYCLE-2 |

## 최종 검증

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

Expected: all commands exit 0. Fresh `-count=1` is required for the focused workflow package; package cache is acceptable for the final `go test ./...` rerun if Go reports cached packages.

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