<!-- task=m-workflow-core/03+01_failure_policy plan=0 tag=WFC-FAILURE -->

# Workflow Core Failure Policy Plan

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

이 계획은 `01_lifecycle_model` PASS 후 진행한다. 구현 완료는 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션과 실제 검증 출력 작성까지다. 사용자 결정이나 외부 환경 준비 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청`을 채운다.

## 배경

현재 실패는 DB `error`와 metadata `agent_run_state/status_reason`에 일부 기록되지만 timeout, execution error, enqueue failure를 구분하는 canonical metadata가 없다. Workflow Core는 실패와 timeout이 관찰 가능한 상태, metadata, error로 남아야 한다. 이 작업은 DB migration 없이 기존 `metadata`/`error` 필드를 활용해 실패 사유를 구조화한다.

## 사용자 리뷰 요청 흐름

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

## Roadmap Targets

- Milestone: `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- Task ids:
  - `failure-policy`: 실패 task 처리 기준
- 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`
- `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/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_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`

### 테스트 환경 규칙

- test_env: `local`
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/core-smoke.md`
- 적용 명령: `cd services/core && go test ./...`
- focused 명령: `cd services/core && go test -count=1 ./internal/workflow ./internal/scheduler`

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

- `TestWorkMarksTimeoutFailure`는 timeout message만 확인한다.
- lifecycle failure metadata는 직접 검증되지 않는다.
- enqueue failure가 `error`와 metadata에 같은 canonical 사유를 남기는지는 후속 ownership 작업 이후 검증 가능하다.

### 심볼 참조

- planned new symbols: `MetadataKeyFailureType`, `MetadataKeyFailedAt`, optional `FailureType`.
- existing references: `FailTask` is used by `scheduler/jobs.go`, `workflow/service.go`, fake lifecycle tests, and storage wrapper.

### 분할 판단

- task group: `m-workflow-core`
- this subtask: `03+01_failure_policy`
- predecessor: `01_lifecycle_model`; active plan exists, `complete.log` pending.
- split reason: failure metadata can be reviewed independently from ownership refactor and retry policy.

### 범위 결정 근거

- No DB migration: `tasks.error` and `tasks.metadata` already exist.
- No provider projection: Plane/Jira status/comment projection is External Integration or later control-plane work.
- No retry limit changes: retry is `04+03_retry_timeout`.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`
- 근거: failure metadata is central to observability and later retry/notification behavior.

## 구현 체크리스트

- [ ] [WFC-FAILURE-1] failure metadata keys and lifecycle failure input/wrapper를 추가한다.
- [ ] [WFC-FAILURE-2] scheduler timeout/error paths가 structured failure type을 남기도록 연결한다.
- [ ] 중간 및 최종 검증 명령을 실행하고 출력 원문을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `01_lifecycle_model` complete.log가 필요하다.
- Runtime dependency source: directory name `03+01_failure_policy`.

## [WFC-FAILURE-1] Structured Failure Metadata

### 문제

- [services/core/internal/workflow/model.go](/config/workspace/nomadcode/services/core/internal/workflow/model.go:31)는 agent state, wait type, reason, heartbeat, attempt key만 정의한다.
- [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:167)는 failed metadata에 `agent_run_state`와 `status_reason`만 쓴다.

### 해결 방법

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

```go
const (
	MetadataKeyAgentRunState = "agent_run_state"
	MetadataKeyAgentPhase    = "agent_phase"
	MetadataKeyWaitType      = "wait_type"
	MetadataKeyStatusReason  = "status_reason"
	MetadataKeyLastHeartbeat = "last_heartbeat_at"
	MetadataKeyPlanRef       = "plan_ref"
	MetadataKeyAttempt       = "attempt"
)
```

After:

```go
const (
	MetadataKeyAgentRunState = "agent_run_state"
	MetadataKeyAgentPhase    = "agent_phase"
	MetadataKeyWaitType      = "wait_type"
	MetadataKeyStatusReason  = "status_reason"
	MetadataKeyFailureType   = "failure_type"
	MetadataKeyFailedAt      = "failed_at"
	MetadataKeyLastHeartbeat = "last_heartbeat_at"
	MetadataKeyPlanRef       = "plan_ref"
	MetadataKeyAttempt       = "attempt"
)
```

Add a small `FailureInput` or equivalent unexported helper so existing `FailTask(ctx,id,message)` remains compatible but can delegate to `FailTaskWithMetadata(ctx,id,input)`.

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

- [ ] `services/core/internal/workflow/model.go`: add failure metadata constants and failure type constants.
- [ ] `services/core/internal/workflow/lifecycle.go`: add failure helper, trim empty messages to stable fallback, set `wait_type=nil`, `failure_type`, and `failed_at`.
- [ ] `services/core/internal/workflow/service_test.go`: fake-store tests assert DB error string and metadata keys.

### 테스트 작성

- Required:
  - `TestLifecycleFailTaskStoresFailureMetadataAndError`
  - `TestLifecycleFailTaskClearsWaitType`

### 중간 검증

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

Expected: failure metadata tests pass.

## [WFC-FAILURE-2] Scheduler Timeout Classification

### 문제

- [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:330) converts `context.DeadlineExceeded` to message `"timeout"` but does not preserve timeout as a structured metadata type.

### 해결 방법

Before [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:329):

```go
msg := err.Error()
if errors.Is(err, context.DeadlineExceeded) {
	msg = "timeout"
}

task, failErr := w.Lifecycle.FailTask(ctx, taskID, msg)
```

After:

```go
msg := err.Error()
failureType := workflow.FailureTypeExecution
if errors.Is(err, context.DeadlineExceeded) {
	msg = "timeout"
	failureType = workflow.FailureTypeTimeout
}

task, failErr := w.Lifecycle.FailTaskWithMetadata(ctx, taskID, workflow.FailureInput{
	Message: msg,
	Type:    failureType,
})
```

If adding a new method to `TaskLifecycle`, update `fakeTaskLifecycle` in scheduler tests.

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

- [ ] `services/core/internal/scheduler/jobs.go`: classify timeout vs execution failure.
- [ ] `services/core/internal/scheduler/jobs_test.go`: fake lifecycle records failure type; timeout test asserts `timeout`.

### 테스트 작성

- Required:
  - Update `TestWorkMarksTimeoutFailure` to assert structured timeout classification.
  - Add or update failure test for non-timeout execution error.

### 중간 검증

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

Expected: timeout and failure tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/workflow/model.go` | WFC-FAILURE-1 |
| `services/core/internal/workflow/lifecycle.go` | WFC-FAILURE-1 |
| `services/core/internal/workflow/service_test.go` | WFC-FAILURE-1 |
| `services/core/internal/scheduler/jobs.go` | WFC-FAILURE-2 |
| `services/core/internal/scheduler/jobs_test.go` | WFC-FAILURE-2 |

## 최종 검증

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

Expected: focused and full tests pass. Fresh `-count=1` is required for focused packages; final full run may use cache.

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