<!-- task=m-milestone-work-item-creation-sync/07+06_workspace_provision_pipeline plan=0 tag=API -->

# Workspace Provision Pipeline Plan - API

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 필수 최종 단계다. 구현 후 검증을 실행하고 실제 변경 내용, 설계 결정, 검증 출력을 review stub에 기록한 뒤 active 파일을 남겨 리뷰를 요청한다. 최종 archive, `complete.log`, 로그 rename은 code-review 전용이다. 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.

## 배경

`workspace-provision`의 남은 부분은 first-use 경로에서 `branches/main`, `branches/develop`, 최소 `slots/000`이 준비되어 있는지 확인하고, 기본 slot을 DB에 등록한 뒤에만 slot 예약과 provider fetch를 진행하는 것이다. 현재 pipeline은 project sync config와 slot 예약은 연결하지만, prepared checkout 확인과 default slot 등록을 같은 runtime boundary에 묶지 않는다. 이 계획은 `06_workspace_provision`의 provisioner가 PASS된 뒤 pipeline/HTTP/storage adapter에 연결해 roadmap Task 완료를 목표로 한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 필요한 경우 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Task ids:
  - `workspace-provision`: project sync 설정 저장 또는 첫 사용 시 `branches/main`, `branches/develop`, 최소 `slots/000`의 checkout 준비 기준을 남긴다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/01_workspace_topology/complete.log`
- `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/02+01_workspace_slot_store/complete.log`
- `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/03+01,02_project_binding_checkout/complete.log`
- `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/03+02_develop_match/complete.log`
- `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/04+03_plane_todo_projection/complete.log`
- `services/core/internal/projectsync/config.go`
- `services/core/internal/projectsync/checkout.go`
- `services/core/internal/projectsync/config_test.go`
- `services/core/internal/projectsync/checkout_test.go`
- `services/core/internal/workitempipeline/service.go`
- `services/core/internal/workitempipeline/service_test.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/storage/project_sync_settings.go`
- `services/core/internal/storage/workspace_slots.go`
- `services/core/internal/storage/store.go`
- `services/core/internal/storage/project_sync_store_test.go`
- `services/core/internal/storage/workspace_slots_test.go`
- `services/core/internal/db/models.go`
- `services/core/internal/db/project_sync_settings.sql.go`
- `services/core/internal/db/workspace_slots.sql.go`
- `services/core/queries/project_sync_settings.sql`
- `services/core/queries/workspace_slots.sql`
- `services/core/migrations/00004_create_project_sync_settings.sql`
- `services/core/migrations/00005_create_workspace_slots.sql`
- `services/core/cmd/server/main.go`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다. `services/core/**` 변경이므로 `agent-test/local/core-smoke.md`를 적용한다.
- `agent-roadmap/**`, `agent-task/**` 변경이 함께 있으므로 `agent-test/local/workspace-ops-smoke.md`도 적용한다.
- core 필수 검증은 `cd services/core && go test ./...`; focused 변경은 `go test -count=1`로 실제 재실행한다. roadmap/task 문서 검증은 `git diff --check`다.
- 규칙/프로필은 구조적으로 비어 있지 않았고 `<확인 필요>` 값은 기준 출력 예시뿐이다.

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

- work item pipeline은 project binding, invalid config, no slot, checkout metadata를 검증하지만 provision failure before reservation/provider fetch를 검증하지 않는다.
- HTTP binder는 resolve/reserve error propagation을 검증하지만 default slot upsert와 provisioner failure propagation을 검증하지 않는다.
- HTTP service error mapping은 project sync missing/no slot을 검증하지만 `ErrWorkspaceProvisionNotReady` mapping이 없다.

### 심볼 참조

- 이름 변경/삭제 없음.
- `ProjectBinder` interface가 확장되면 모든 구현체를 갱신해야 한다.
- 참조 위치:
  - `services/core/internal/workitempipeline/service.go`: `ProjectBinder` 정의와 `CreateTaskFromWorkItem` call site.
  - `services/core/internal/workitempipeline/service_test.go`: `fakeBinder`, `mapBinder`.
  - `services/core/internal/http/handlers.go`: `projectSyncBinder`.
  - `services/core/internal/http/handlers_test.go`: `fakeProjectSyncStore`, binder tests.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 task group: `m-milestone-work-item-creation-sync`.
- sibling 계획:
  - `06_workspace_provision`: provisioner foundation.
  - `07+06_workspace_provision_pipeline`: pipeline/HTTP/storage rollout.
- predecessor index `06`: active sibling `agent-task/m-milestone-work-item-creation-sync/06_workspace_provision/complete.log`가 아직 없다. 이 계획은 해당 complete.log가 생긴 뒤 구현해야 한다.

### 범위 결정 근거

- 이 계획은 git clone/fetch/checkout 실행기를 만들지 않는다. prepared checkout 확인은 `06_workspace_provision`의 provisioner가 담당한다.
- project setting 저장 API/UI hook은 현재 codebase에 없으므로 first-use path만 연결한다. setting-save hook은 후속 settings UI/API slice에서 같은 provisioner를 호출하면 된다.
- SQL/migration/query 변경은 하지 않는다. 기본 slot 등록은 기존 `UpsertWorkspaceSlot` query를 사용한다.
- Plane projection, develop match, idempotency/retry 흐름은 이미 별도 완료 로그가 있으므로 수정하지 않는다.

### 빌드 등급

- `cloud-G07`: internal interface 확장, storage side effect, HTTP error mapping, runtime ordering이 결합된 cross-package 변경이다.

## 의존 관계 및 구현 순서

- `07+06_workspace_provision_pipeline`은 `06_workspace_provision`에만 의존한다.
- 구현 시작 전 `agent-task/m-milestone-work-item-creation-sync/06_workspace_provision/complete.log` 또는 matching archive complete.log가 있어야 한다.
- 이 계획 파일 작성 시점에는 predecessor complete.log가 없다.

## 구현 체크리스트

- [ ] `workitempipeline.ProjectBinder`에 project workspace provision step을 추가하고 `CreateTaskFromWorkItem`이 slot 예약/provider fetch 전에 호출하도록 한다.
- [ ] HTTP project sync binder가 provisioner를 실행하고 기본 `slots/000` row를 기존 `UpsertWorkspaceSlot`로 등록하도록 연결한다.
- [ ] provision failure를 HTTP service error로 명시 매핑하고 pipeline/HTTP 회귀 테스트를 추가한다.
- [ ] focused/final 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Pipeline Provision Step Before Slot Reservation

- `문제`: [services/core/internal/workitempipeline/service.go](/config/workspace/nomadcode/services/core/internal/workitempipeline/service.go:38)의 `ProjectBinder`는 resolve/reserve만 제공한다. [services/core/internal/workitempipeline/service.go](/config/workspace/nomadcode/services/core/internal/workitempipeline/service.go:77)는 config normalize 후 곧바로 slot을 예약하므로 prepared checkout 실패를 예약 전에 막을 hook이 없다.

Before (`services/core/internal/workitempipeline/service.go:38`):

```go
// ProjectBinder resolves the active project sync binding for a work item's
// provider/project target and reserves an independent workspace slot for it.
// Implementations must key the binding strictly on the normalized
// provider/tenant/project so tickets from the same project share one config and
// never mix with another project's config, and must reserve one available slot
// atomically so concurrent requests never receive the same slot.
type ProjectBinder interface {
	ResolveProjectBinding(ctx context.Context, ref workitem.Ref) (ProjectBinding, error)
	ReserveWorkspaceSlot(ctx context.Context, projectSyncSettingID int64) (projectsync.WorkspaceSlot, error)
}
```

Before (`services/core/internal/workitempipeline/service.go:88`):

```go
	config, err := binding.Config.Normalize()
	if err != nil {
		return storage.Task{}, err
	}
	slot, err := s.binder.ReserveWorkspaceSlot(ctx, binding.SettingID)
```

- `해결 방법`: `ProjectBinder`에 `EnsureProjectWorkspace(ctx context.Context, binding ProjectBinding) error`를 추가한다. `CreateTaskFromWorkItem`은 config normalize 뒤, `ReserveWorkspaceSlot` 전, provider fetch 전 이 hook을 호출한다. provision error면 slot 예약, provider fetch, task creation을 모두 수행하지 않는다.

After sketch:

```go
type ProjectBinder interface {
	ResolveProjectBinding(ctx context.Context, ref workitem.Ref) (ProjectBinding, error)
	EnsureProjectWorkspace(ctx context.Context, binding ProjectBinding) error
	ReserveWorkspaceSlot(ctx context.Context, projectSyncSettingID int64) (projectsync.WorkspaceSlot, error)
}
```

```go
	config, err := binding.Config.Normalize()
	if err != nil {
		return storage.Task{}, err
	}
	if err := s.binder.EnsureProjectWorkspace(ctx, binding); err != nil {
		return storage.Task{}, err
	}
	slot, err := s.binder.ReserveWorkspaceSlot(ctx, binding.SettingID)
```

- `수정 파일 및 체크리스트`:
  - [ ] `services/core/internal/workitempipeline/service.go`: interface 확장, ordering 변경, comment 갱신.
  - [ ] `services/core/internal/workitempipeline/service_test.go`: fake/map binder 구현 갱신, provision failure regression 추가.
- `테스트 작성`: `TestCreateTaskFromWorkItemBlocksWhenWorkspaceProvisionFails`를 추가한다. assertion: provision error가 반환되고 `reserveCalled=false`, `reader.fetchCalled=false`, `creator.calledWith=nil`.
- `중간 검증`: `cd services/core && go test -count=1 ./internal/workitempipeline`가 PASS해야 한다.

### [API-2] HTTP Binder Provision And Default Slot Registration

- `문제`: [services/core/internal/http/handlers.go](/config/workspace/nomadcode/services/core/internal/http/handlers.go:59)의 `ProjectSyncStore`는 active setting lookup과 reserve만 갖고 있다. [services/core/internal/http/handlers.go](/config/workspace/nomadcode/services/core/internal/http/handlers.go:77)의 binder는 provision plan을 실행하지 않고 default slot도 first-use에서 등록하지 않는다.

Before (`services/core/internal/http/handlers.go:59`):

```go
// ProjectSyncStore is the narrow storage dependency the project sync binder needs
// to look up the active setting for a provider/project target and to reserve an
// available workspace slot for it.
type ProjectSyncStore interface {
	GetActiveProjectSyncSettingByTarget(ctx context.Context, provider, tenant, project string) (db.ProjectSyncSetting, error)
	ReserveWorkspaceSlot(ctx context.Context, projectSyncSettingID int64) (db.WorkspaceSlot, error)
}
```

Before (`services/core/internal/http/handlers.go:77`):

```go
func NewProjectBinder(store ProjectSyncStore) workitempipeline.ProjectBinder {
	if store == nil {
		return nil
	}
	return projectSyncBinder{store: store}
}
```

- `해결 방법`: `ProjectSyncStore`에 `UpsertWorkspaceSlot(ctx, db.UpsertWorkspaceSlotParams)`를 추가한다. `projectSyncBinder`에 `projectsync.WorkspaceProvisioner`를 주입하고 `NewProjectBinder(store)`는 filesystem provisioner를 기본값으로 쓴다. 테스트용으로 `NewProjectBinderWithProvisioner(store, provisioner)`를 추가한다. `EnsureProjectWorkspace`는 config normalize, `BuildProvisionPlan`, `provisioner.EnsureProvisioned`, `ToUpsertWorkspaceSlotParams(settingID, DefaultSlotIndex, plan.DefaultSlotPath)`, `store.UpsertWorkspaceSlot` 순서로 실행한다.

After sketch:

```go
type ProjectSyncStore interface {
	GetActiveProjectSyncSettingByTarget(ctx context.Context, provider, tenant, project string) (db.ProjectSyncSetting, error)
	UpsertWorkspaceSlot(ctx context.Context, args db.UpsertWorkspaceSlotParams) (db.WorkspaceSlot, error)
	ReserveWorkspaceSlot(ctx context.Context, projectSyncSettingID int64) (db.WorkspaceSlot, error)
}
```

```go
func NewProjectBinder(store ProjectSyncStore) workitempipeline.ProjectBinder {
	return NewProjectBinderWithProvisioner(store, projectsync.FilesystemWorkspaceProvisioner{})
}
```

- `수정 파일 및 체크리스트`:
  - [ ] `services/core/internal/http/handlers.go`: store interface, binder fields/constructors, `EnsureProjectWorkspace` 구현.
  - [ ] `services/core/internal/http/handlers_test.go`: fake store/provisioner 갱신, default slot upsert/provision error tests.
- `테스트 작성`: `TestNewProjectBinderEnsuresWorkspaceAndRegistersDefaultSlot`와 `TestNewProjectBinderPropagatesProvisionErrorWithoutUpsert`를 추가한다. assertion: default slot path는 `BuildProvisionPlan`의 `DefaultSlotPath`, provision error면 upsert/reserve가 호출되지 않는다.
- `중간 검증`: `cd services/core && go test -count=1 ./internal/http`가 PASS해야 한다.

### [API-3] Provision Error HTTP Mapping

- `문제`: [services/core/internal/http/handlers.go](/config/workspace/nomadcode/services/core/internal/http/handlers.go:329)의 service error mapping은 project sync missing/no slot만 구분한다. workspace checkout 준비 실패는 default 500으로 떨어질 수 있다.

Before (`services/core/internal/http/handlers.go:329`):

```go
func (h *Handler) writeServiceError(w stdhttp.ResponseWriter, err error) {
	switch {
	case errors.Is(err, workflow.ErrInvalidTaskInput):
		writeError(w, stdhttp.StatusBadRequest, err.Error())
	case errors.Is(err, workflow.ErrTaskCannotBeEnqueued):
		writeError(w, stdhttp.StatusConflict, err.Error())
	case errors.Is(err, workitempipeline.ErrProjectSyncNotConfigured):
		writeError(w, stdhttp.StatusServiceUnavailable, "project sync resolver is not configured")
	case errors.Is(err, storage.ErrProjectSyncNotFound):
		writeError(w, stdhttp.StatusUnprocessableEntity, "no active project sync setting for the work item's project")
	case errors.Is(err, storage.ErrNoAvailableWorkspaceSlot):
		writeError(w, stdhttp.StatusConflict, "no available workspace slot")
	case errors.Is(err, pgx.ErrNoRows):
```

- `해결 방법`: `projectsync.ErrWorkspaceProvisionNotReady`를 `409 Conflict`로 매핑하고, response message는 raw path leak을 피하면서 원인을 알 수 있게 `"workspace provision is not ready"`로 둔다.

After sketch:

```go
	case errors.Is(err, projectsync.ErrWorkspaceProvisionNotReady):
		writeError(w, stdhttp.StatusConflict, "workspace provision is not ready")
```

- `수정 파일 및 체크리스트`:
  - [ ] `services/core/internal/http/handlers.go`: error mapping 추가.
  - [ ] `services/core/internal/http/handlers_test.go`: create work item route가 provision error에 409를 반환하는 test 추가.
- `테스트 작성`: `TestCreateWorkItemTaskReturns409WhenWorkspaceProvisionNotReady`를 추가한다.
- `중간 검증`: `cd services/core && go test -count=1 ./internal/http`가 PASS해야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/workitempipeline/service.go` | API-1 |
| `services/core/internal/workitempipeline/service_test.go` | API-1 |
| `services/core/internal/http/handlers.go` | API-2, API-3 |
| `services/core/internal/http/handlers_test.go` | API-2, API-3 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/projectsync ./internal/workitempipeline ./internal/http
```

예상: PASS. predecessor package와 rollout packages fresh execution required.

```bash
cd services/core && go test ./...
```

예상: PASS. broad baseline은 cache 출력 허용.

```bash
cd services/core && go vet ./...
```

예상: PASS.

```bash
git diff --check
```

예상: 출력 없음.

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