<!-- task=m-milestone-work-item-creation-sync/11_creation_sync_orchestrator plan=0 tag=REVIEW_SYNC -->

# Plan - REVIEW_SYNC Creation Sync Orchestrator

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 필수 마지막 단계다. 구현 후 검증 명령의 실제 출력, 계획 대비 변경 사항, 주요 설계 결정을 기록하고 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 외부 환경 전제, scope 충돌, 또는 안전한 진행을 막는 결정이 생기면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 사항, 근거, 실행한 명령과 출력, 재개 조건을 기록하고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 마일스톤은 Plane-origin Milestone 생성 sync를 `Todo` 검토 상태까지 수렴시킨다고 되어 있지만, 코드레벨 검토 결과 helper와 저장소만 있고 이를 호출하는 런타임 orchestration 경로가 없다. `roadmapsync`의 match/projection/retry 함수, Plane adapter projection facet, storage ledger는 각각 테스트되어 있으나 develop scan 결과에서 Plane comment/body/status projection으로 이어지는 caller가 없다. 따라서 마일스톤 완료 대신 이 보완 작업을 먼저 구현해야 한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사항은 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 채팅에서 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 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:
  - `creation-sync-orchestrator`: develop Milestone match, identity/step ledger, Plane 원문 보존/본문·제목 갱신/`Todo` 이동 runtime sync orchestrator
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 규칙/로드맵:
  - `agent-ops/rules/project/rules.md`
  - `agent-ops/rules/private/rules.md`
  - `agent-ops/rules/common/rules-roadmap.md`
  - `agent-ops/skills/common/router.md`
  - `agent-ops/skills/common/code-review/SKILL.md`
  - `agent-ops/skills/common/update-roadmap/SKILL.md`
  - `agent-ops/skills/common/analyze-roadmap-position/SKILL.md`
  - `agent-ops/skills/common/plan/SKILL.md`
  - `agent-ops/skills/common/_templates/roadmap-position-report-template.md`
  - `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
  - `agent-roadmap/ROADMAP.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-ops/rules/project/domain/core/rules.md`
  - `agent-ops/rules/project/domain/contracts/rules.md`
  - `agent-ops/rules/project/domain/workspace-ops/rules.md`
- 테스트 규칙:
  - `agent-test/local/rules.md`
  - `agent-test/local/core-smoke.md`
  - `agent-test/local/contracts-smoke.md`
  - `agent-test/local/workspace-ops-smoke.md`
- 소스:
  - `services/core/cmd/server/main.go`
  - `services/core/internal/adapters/openai/client.go`
  - `services/core/internal/adapters/plane/client.go`
  - `services/core/internal/authoring/request.go`
  - `services/core/internal/authoring/result.go`
  - `services/core/internal/db/models.go`
  - `services/core/internal/http/handlers.go`
  - `services/core/internal/http/router.go`
  - `services/core/internal/model/model.go`
  - `services/core/internal/projectsync/checkout.go`
  - `services/core/internal/projectsync/provision.go`
  - `services/core/internal/roadmapsync/develop_match.go`
  - `services/core/internal/roadmapsync/identity.go`
  - `services/core/internal/roadmapsync/milestone_parser.go`
  - `services/core/internal/roadmapsync/plane_format.go`
  - `services/core/internal/roadmapsync/plane_projection.go`
  - `services/core/internal/roadmapsync/retry.go`
  - `services/core/internal/scheduler/jobs.go`
  - `services/core/internal/scheduler/river.go`
  - `services/core/internal/storage/roadmap_sync_identities.go`
  - `services/core/internal/storage/roadmap_sync_steps.go`
  - `services/core/internal/storage/store.go`
  - `services/core/internal/workflow/model.go`
  - `services/core/internal/workflow/lifecycle.go`
  - `services/core/internal/workitem/provider.go`
  - `services/core/internal/workitempipeline/service.go`
- 테스트:
  - `services/core/internal/adapters/openai/client_test.go`
  - `services/core/internal/adapters/plane/client_test.go`
  - `services/core/internal/http/handlers_test.go`
  - `services/core/internal/roadmapsync/develop_match_test.go`
  - `services/core/internal/roadmapsync/identity_test.go`
  - `services/core/internal/roadmapsync/milestone_parser_test.go`
  - `services/core/internal/roadmapsync/plane_format_test.go`
  - `services/core/internal/roadmapsync/plane_projection_test.go`
  - `services/core/internal/roadmapsync/retry_test.go`
  - `services/core/internal/scheduler/jobs_test.go`
  - `services/core/internal/storage/roadmap_sync_identities_test.go`
  - `services/core/internal/storage/roadmap_sync_steps_test.go`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`가 존재해 전체를 읽었다.
- 매칭 profile로 `core-smoke`, `contracts-smoke`, `workspace-ops-smoke`를 읽었다. 이번 변경은 Core 런타임 orchestration이며 Flutter/mobile 검증은 대상이 아니다.
- 적용 명령:
  - 중간: `cd services/core && go test -count=1 ./internal/roadmapsync ./internal/storage`
  - 중간: `cd services/core && go test -count=1 ./internal/scheduler ./internal/http ./internal/adapters/plane`
  - 최종: `cd services/core && go test -count=1 ./...`
  - 최종: `cd services/core && go vet ./...`
  - 최종: `git diff --check`
- fresh 실행이 중요하므로 최종 `go test`는 cache 출력이 아니라 `-count=1`을 사용한다.
- 기존 검토 중 `cd services/core && go test ./...`, `cd services/core && go vet ./...`, `git diff --check`는 PASS였지만 helper 미연결을 잡지 못했으므로 이 계획의 PASS 근거로 충분하지 않다.

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

- `MatchDevelopMilestone`, `ParseMilestoneIdentity`, `ProjectPlaneTodo`, `FormatPlaneProjection`, `ReconcileCreationCycle`, storage identity/step ledger는 helper 또는 storage 단위 테스트가 있다.
- `workitempipeline`과 `scheduler` 테스트는 Plane trigger gate와 authoring bridge route를 다룬다.
- 공백: develop scan 결과가 identity upsert, completed-step ledger, actor/revision guard, Plane comment/body/status projection을 단계별로 호출한다는 orchestrator/caller 테스트가 없다.
- 공백: `StepOriginalCommentPreserved`, `StepPlaneBodyUpdated`, `StepPlaneTodoMoved`가 provider 성공 직후 각각 기록되고 재시도에서 건너뛰는지 검증하는 테스트가 없다.
- 공백: `scheduler.New` 또는 server wiring에서 roadmap creation sync worker/service가 등록되어 실제 runtime path가 생기는지 검증하는 테스트가 없다.

### 심볼 참조

- 이름 변경 또는 제거 예정 심볼: 없음.
- 새 symbol을 추가한다. 기존 `ProjectPlaneTodo`, `ReconcileCreationCycle`, `UpsertRoadmapSyncIdentity`, `CompletedSteps`, `MarkStepCompleted`, `MatchDevelopMilestone`, `ParseMilestoneIdentity`는 유지한다.
- 현재 `rg --sort path` 결과 주요 helper는 package test와 storage wrapper 외 runtime caller가 없다. 이 계획의 핵심은 호출부 추가다.

### 분할 판단

- split decision policy를 계획 파일 선택 전에 평가했다.
- 이 작업은 `m-milestone-work-item-creation-sync/11_creation_sync_orchestrator` 한 subtask의 단일 plan으로 둔다.
- foundation과 call-site가 자연스럽게 보이지만, helper API만 먼저 끝내면 마일스톤을 막은 문제인 "실제 호출 경로 없음"이 해소되지 않는다. provider/store fake 기반 orchestrator 테스트와 scheduler/server wiring 테스트를 한 리뷰 단위에서 보는 편이 더 안전하다.
- ownership은 Core sync runtime 하나이며, 새 DB migration이나 public HTTP/contract 변경을 기본 범위로 잡지 않는다.
- 선행 subtask dependency는 없다. `agent-task/archive/**`는 이번 plan 작성에 필요한 predecessor 확인 대상이 아니므로 읽지 않았다.

### 범위 결정 근거

- Plane-origin 생성 sync만 다룬다. Jira, mobile UI, project settings UI, `Todo -> In Progress` 실행 lifecycle은 제외한다.
- 새 public HTTP API는 기본 범위에서 제외한다. develop scan producer가 반드시 process boundary를 넘어야 해서 public route가 필요해지면 contracts/domain rule을 다시 확인하고 `사용자 리뷰 요청` 또는 별도 follow-up으로 멈춘다.
- 새 DB migration은 기본 범위에서 제외한다. 기존 `roadmap_sync_identities`와 `roadmap_sync_steps`로 identity/retry를 구현한다.
- helper 단위 테스트를 단순히 더 추가하는 것으로 완료 처리하지 않는다. runtime caller 또는 worker/service wiring 테스트가 있어야 한다.

### 빌드 등급

- `cloud-G07`: Core runtime, provider side effect, storage idempotency, scheduler/server wiring을 함께 판단해야 하고 기존 테스트가 helper-level에 치우쳐 있어 larger context와 강한 리뷰가 필요하다.

## 구현 체크리스트

- [ ] [REVIEW_SYNC-1] develop scan candidate를 identity/step ledger와 Plane projection 단계에 연결하는 creation sync orchestrator를 구현한다.
- [ ] [REVIEW_SYNC-2] orchestrator가 dead code로 남지 않도록 scheduler/server runtime wiring 또는 기존 develop scan handoff에 연결하고 테스트로 증명한다.
- [ ] 중간 및 최종 검증 명령을 fresh 실행하고 실제 stdout/stderr를 `CODE_REVIEW-cloud-G07.md`에 붙여 넣는다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_SYNC-1] Creation Sync Orchestrator

### 문제

- `services/core/internal/roadmapsync/develop_match.go:44`는 develop scan candidate 준비 여부만 판단한다.
- `services/core/internal/roadmapsync/retry.go:105`는 step ledger 기반 next action만 결정한다.
- `services/core/internal/storage/roadmap_sync_identities.go:67`와 `services/core/internal/storage/roadmap_sync_steps.go:50`는 identity/step 저장소 wrapper만 제공한다.
- `services/core/internal/roadmapsync/plane_projection.go:62`는 Plane projection을 한 번에 실행하지만, 현재 코드에는 위 helper들을 묶어 호출하고 각 단계 성공 직후 ledger를 기록하는 orchestrator가 없다.

현재 before:

```go
// services/core/internal/roadmapsync/plane_projection.go:77
if err := p.AppendComment(ctx, workitem.CommentInput{
    Ref:            in.Ref,
    Body:           preserveCommentPrefix + " " + in.OriginalBody,
    Format:         workitem.CommentFormatHTML,
    ExternalSource: in.ExternalSource,
    ExternalID:     in.ExternalID,
}); err != nil {
    return err
}

if err := p.ProjectBody(ctx, workitem.BodyProjection{
    Ref:             in.Ref,
    Title:           projection.Title,
    DescriptionHTML: projection.BodyHTML,
}); err != nil {
    return err
}

return p.ProjectStatus(ctx, workitem.StatusProjection{
    Ref:           in.Ref,
    ProviderState: in.TodoStateID,
})
```

### 해결 방법

- 새 side-effect package를 `services/core/internal/roadmapsyncpipeline/`에 만든다. 기존 `roadmapsync` package는 provider-neutral helper 중심으로 유지한다.
- `Service.SyncCreation(ctx, input)`을 추가한다. input은 expected identity, scan result, work item ref, original Plane body, Todo state id, Milestone markdown, actor/self actor, provider/roadmap revision, external source/id를 가진다.
- `ParseMilestoneIdentity`가 필요한 경우 scan result 구성 전에 사용하거나 service input에서 `ScannedMilestones`가 비어 있을 때만 milestone markdown/path에서 identity를 보강한다.
- 기존 identity를 work item 기준으로 찾고 없으면 upsert한다. 이후 `CompletedSteps`를 읽어 `ReconcileCreationCycle`에 전달한다.
- `ActionSkipSelfMutation`, `ActionConflict`, not-ready candidate는 provider side effect 없이 typed result를 반환한다.
- ready + resume이면 다음 순서로 실행하고 각 provider 성공 직후 ledger를 기록한다.
  - `StepDevelopMatched`
  - `StepOriginalCommentPreserved`
  - `StepPlaneBodyUpdated`
  - `StepPlaneTodoMoved`
- `ProjectPlaneTodo`는 기존 공개 동작을 유지한다. orchestrator가 단계별 ledger를 기록할 수 있도록 projection formatting과 provider step 호출을 재사용 가능한 unexported helper로 쪼개거나, 동등한 단계별 호출을 orchestrator 안에 구현한다. 기존 `ProjectPlaneTodo` 테스트는 계속 통과해야 한다.

목표 after:

```go
candidate := roadmapsync.MatchDevelopMilestone(roadmapsync.MatchInput{
    Scan:     in.Scan,
    Expected: in.Expected,
})
if !candidate.Ready {
    return SyncCreationResult{Action: SyncActionNotReady, Reason: candidate.Reason}, nil
}

identityRow, completed, err := s.loadOrUpsertIdentityAndSteps(ctx, in, candidate)
if err != nil {
    return SyncCreationResult{}, err
}
decision := roadmapsync.ReconcileCreationCycle(roadmapsync.ReconcileInput{
    Identity:         candidate.Identity,
    ProviderRevision: in.ProviderRevision,
    RoadmapRevision:  in.RoadmapRevision,
    Actor:            in.Actor,
    SelfActor:        in.SelfActor,
    Existing:         toExistingIdentity(identityRow),
    CompletedSteps:   toRoadmapSyncCompleted(completed),
})
if decision.Action != roadmapsync.ActionResume {
    return resultFromDecision(decision), nil
}

for _, step := range remainingSteps(decision.NextStep, completed) {
    if err := s.runStep(ctx, step, in, candidate); err != nil {
        return SyncCreationResult{Action: SyncActionRetryable, NextStep: step}, err
    }
    if _, err := s.store.MarkStepCompleted(ctx, identityRow.ID, toStorageStep(step)); err != nil {
        return SyncCreationResult{}, err
    }
}
return SyncCreationResult{Action: SyncActionComplete}, nil
```

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

- [ ] `services/core/internal/roadmapsyncpipeline/service.go`를 추가한다.
- [ ] `services/core/internal/roadmapsyncpipeline/service_test.go`를 추가한다.
- [ ] 필요한 경우 `services/core/internal/roadmapsync/plane_projection.go` 내부 provider step helper를 분리하되 기존 API와 테스트를 유지한다.
- [ ] 필요한 경우 `services/core/internal/roadmapsync/plane_projection_test.go`에 기존 `ProjectPlaneTodo` regression을 보강한다.

### 테스트 작성

- 작성: `services/core/internal/roadmapsyncpipeline/service_test.go`
  - `TestSyncCreationNotReadyDoesNotCallProvider`: develop match not-ready면 provider 호출과 ledger mark가 없다.
  - `TestSyncCreationReadyRunsStepsAndMarksLedger`: ready scan이면 comment, body, status 순서와 step ledger 기록 순서가 일치한다.
  - `TestSyncCreationRetrySkipsCompletedSteps`: completed ledger가 있으면 남은 단계만 실행한다.
  - `TestSyncCreationStopsBeforeLaterStepOnProviderError`: 중간 provider 실패 시 이후 provider 호출과 ledger mark가 없다.
  - `TestSyncCreationSkipsSelfMutation`: actor guard가 provider 호출을 막는다.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline
```

기대 결과: fresh test PASS. `./internal/roadmapsyncpipeline`가 새 package로 존재하지 않는 중간 상태에서 이 명령을 통과했다고 기록하면 안 된다.

## [REVIEW_SYNC-2] Runtime Wiring

### 문제

- `services/core/internal/scheduler/river.go:26`의 scheduler constructor는 `TaskWorker`만 등록한다.
- `services/core/cmd/server/main.go:97`은 scheduler를 task lifecycle, notification, agent/model client만으로 구성한다.
- `services/core/internal/scheduler/jobs.go:127`부터의 authoring route는 model bridge 응답 후 `authoring_run_state: "in_progress"` 결과를 반환하고, develop scan/projection orchestration을 호출하지 않는다.
- `services/core/internal/http/router.go:33`부터의 API에는 integrations/tasks/task enqueue만 있고 roadmap sync callback이나 내부 handoff route가 없다.
- 따라서 REVIEW_SYNC-1의 service만 추가하면 여전히 runtime dead code가 된다.

현재 before:

```go
// services/core/internal/scheduler/river.go:26
func New(pool *pgxpool.Pool, lifecycle TaskLifecycle, notifications *notification.Service, agentClient agent.Client, modelClient model.Client, runTimeout time.Duration, logger *slog.Logger) (*Client, error) {
    workers := river.NewWorkers()
    river.AddWorker(workers, &TaskWorker{
        Lifecycle:     lifecycle,
        Notifications: notifications,
        Agent:         agentClient,
        Model:         modelClient,
        RunTimeout:    runTimeout,
        Logger:        logger,
    })
```

### 해결 방법

- `scheduler`에 `RoadmapCreationSyncJobArgs`와 worker를 추가한다. worker는 REVIEW_SYNC-1 service를 호출하고, provider side effect 없이 complete/conflict/not-ready/skip 결과를 구분해 로그와 error를 안정적으로 반환한다.
- `Client`에 `EnqueueRoadmapCreationSync(ctx, args)`를 추가한다. 이 method는 기존 task enqueue와 별도이며 scan handoff가 호출하는 내부 seam이다.
- `scheduler.New`는 선택적 creation sync service를 받아 worker를 등록한다. nil이면 worker를 등록하지 않아 기존 테스트와 기본 task scheduler 동작을 유지한다.
- `cmd/server/main.go`에서 Plane client와 store로 creation sync service를 구성해 scheduler에 전달한다. Plane-origin 범위만 다루며 Jira는 연결하지 않는다.
- 기존 develop scan producer가 있으면 그 producer에서 `EnqueueRoadmapCreationSync`를 호출한다. 현재 repo에 producer가 끝내 없으면 구현 에이전트는 dead worker만으로 완료하지 말고 `사용자 리뷰 요청`에 "runtime scan producer 범위 결정 필요"를 기록한다.
- public HTTP route는 기본 추가하지 않는다. 구현 중 process boundary가 필요하다고 확인되면 contracts/domain rule을 다시 확인하고 별도 범위로 멈춘다.

목표 after:

```go
func New(
    pool *pgxpool.Pool,
    lifecycle TaskLifecycle,
    notifications *notification.Service,
    agentClient agent.Client,
    modelClient model.Client,
    creationSync CreationSyncRunner,
    runTimeout time.Duration,
    logger *slog.Logger,
) (*Client, error) {
    workers := river.NewWorkers()
    river.AddWorker(workers, &TaskWorker{ /* existing fields */ })
    if creationSync != nil {
        river.AddWorker(workers, &RoadmapCreationSyncWorker{
            Sync:   creationSync,
            Logger: logger,
        })
    }
}
```

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

- [ ] `services/core/internal/scheduler/river.go`에 optional sync worker registration과 enqueue method를 추가한다.
- [ ] `services/core/internal/scheduler/jobs.go` 또는 새 `roadmap_sync_jobs.go`에 job args/worker를 추가한다.
- [ ] `services/core/cmd/server/main.go`에서 `roadmapsyncpipeline.Service`를 구성해 scheduler에 주입한다.
- [ ] 기존 develop scan handoff가 확인되면 해당 caller를 연결한다. 없으면 `사용자 리뷰 요청`을 작성하고 dead-code-only PASS를 금지한다.
- [ ] public HTTP route를 추가하지 않은 상태로 끝낼 수 있는지 확인한다. 추가가 필요하면 contracts follow-up 없이 몰래 확장하지 않는다.

### 테스트 작성

- 작성 또는 갱신: `services/core/internal/scheduler/jobs_test.go` 또는 `services/core/internal/scheduler/river_test.go`
  - `TestRoadmapCreationSyncWorkerCallsService`: job args가 service input으로 전달된다.
  - `TestSchedulerRegistersRoadmapSyncWorkerWhenConfigured`: service가 있을 때 worker/enqueue seam이 작동한다.
  - `TestSchedulerWithoutRoadmapSyncKeepsTaskWorkerOnlyBehavior`: nil service에서도 기존 task enqueue 동작이 깨지지 않는다.
- 필요 시 `services/core/cmd/server`는 package test가 없으므로 `go test -count=1 ./...`와 `go vet ./...`로 compile wiring을 검증한다.

### 중간 검증

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

기대 결과: fresh test PASS. scheduler nil-service regression과 sync worker route가 모두 검증되어야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/roadmapsyncpipeline/service.go` | REVIEW_SYNC-1 |
| `services/core/internal/roadmapsyncpipeline/service_test.go` | REVIEW_SYNC-1 |
| `services/core/internal/roadmapsync/plane_projection.go` | REVIEW_SYNC-1 |
| `services/core/internal/roadmapsync/plane_projection_test.go` | REVIEW_SYNC-1 |
| `services/core/internal/scheduler/river.go` | REVIEW_SYNC-2 |
| `services/core/internal/scheduler/jobs.go` 또는 `services/core/internal/scheduler/roadmap_sync_jobs.go` | REVIEW_SYNC-2 |
| `services/core/internal/scheduler/jobs_test.go` 또는 `services/core/internal/scheduler/river_test.go` | REVIEW_SYNC-2 |
| `services/core/cmd/server/main.go` | REVIEW_SYNC-2 |

## 최종 검증

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

기대 결과: fresh test PASS.

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

기대 결과: PASS.

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

기대 결과: 출력 없음.

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