<!-- task=m-plane-origin-authoring-roundtrip-sync/04+03_identity_write plan=0 tag=IDENTITY_WRITE -->

# Plan - IDENTITY_WRITE

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, 로그 아카이브, `complete.log` 작성, task directory archive 이동은 code-review 스킬 전용이다. 선택된 SDD 결정이나 Milestone `구현 잠금 > 결정 필요` 항목이 구현을 막을 때만 review stub의 `사용자 리뷰 요청`을 채우고 멈춘다. 직접 사용자에게 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/service blocker와 증거 공백은 일반 follow-up 또는 검증 결과로 기록한다.

## 배경

`identity-source`, `develop-match`, `idempotency-retry`는 보강과 재검증으로 완료 처리했다. 남은 `identity-write`는 생성된 Milestone 문서에 provider identity block이 없어도 Gito callback 후 조용히 drop되지 않게 만드는 핵심 구현이다. 사용자 프롬프트는 얇게 유지해야 하므로 Core/runtime이 authoring 결과와 push-only 사이에서 identity를 결정적으로 주입한다.

## 사용자 리뷰 요청 흐름

구현 중 새 결정이 필요해 보여도 채팅 질문을 하지 않는다. 선택된 SDD 결정 또는 선택된 Milestone lock 결정만 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록하고 code-review가 검증한다.

## Archive Evidence Snapshot

- Predecessor: `agent-task/archive/2026/06/m-plane-origin-authoring-roundtrip-sync/03+01,02_push_command/complete.log`
- Verdict: PASS
- Carryover: push-only 요청은 authoring 성공 뒤 별도 model call로 실행되며, push 성공 뒤에도 task completion은 develop match까지 대기한다.
- Verification carried: focused Go tests, `cd services/core && go test -count=1 ./...`, `git diff --check`, standard remote runner 검증 통과.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-origin-authoring-roundtrip-sync.md`
- Task ids:
  - `identity-write`: Core/runtime이 provider identity block 또는 동등한 ledger를 만들어 identity 없는 generated Milestone이 조용히 drop되지 않게 한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/plane-origin-authoring-roundtrip-sync/SDD.md`
- Acceptance scenarios:
  - `S06`: task=`identity-write`; evidence=`identity-less generated Milestone does not silently drop; Core/runtime injects deterministic identity or fails explicitly before projection`
- Completion mode: spec-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/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-origin-authoring-roundtrip-sync.md`
- `agent-roadmap/sdd/agent-ops-mcp-control-plane/plane-origin-authoring-roundtrip-sync/SDD.md`
- `agent-task/archive/2026/06/m-plane-origin-authoring-roundtrip-sync/03+01,02_push_command/complete.log`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `services/core/internal/authoring/request.go`
- `services/core/internal/authoring/request_test.go`
- `services/core/internal/authoring/result.go`
- `services/core/internal/authoring/result_test.go`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/internal/scheduler/river.go`
- `services/core/internal/scheduler/river_test.go`
- `services/core/cmd/server/main.go`
- `services/core/cmd/server/main_test.go`
- `services/core/internal/gitosync/scanner.go`
- `services/core/internal/gitosync/scanner_test.go`
- `services/core/internal/gitosync/bridge.go`
- `services/core/internal/gitosync/bridge_test.go`
- `services/core/internal/roadmapsync/develop_match.go`
- `services/core/internal/roadmapsync/develop_match_test.go`
- `services/core/internal/roadmapsync/milestone_parser.go`
- `services/core/internal/roadmapsync/milestone_parser_test.go`
- `services/core/internal/roadmapsync/identity.go`
- `services/core/internal/roadmapsync/identity_test.go`
- `services/core/internal/roadmapsync/retry.go`
- `services/core/internal/roadmapsync/retry_test.go`
- `services/core/internal/roadmapsyncpipeline/service.go`
- `services/core/internal/roadmapsyncpipeline/service_test.go`
- `services/core/internal/storage/roadmap_sync_identities.go`
- `services/core/internal/storage/roadmap_sync_identities_test.go`
- `services/core/internal/storage/roadmap_sync_steps.go`
- `services/core/internal/storage/roadmap_sync_steps_test.go`
- `services/core/internal/workitempipeline/service.go`
- `services/core/internal/workitempipeline/service_test.go`
- `services/core/internal/model/model.go`
- `services/core/internal/adapters/openai/client.go`
- `services/core/internal/adapters/openai/client_test.go`
- `services/core/migrations/00006_create_roadmap_sync_identities.sql`
- `services/core/migrations/00007_create_roadmap_sync_steps.sql`
- `services/core/queries/roadmap_sync_identities.sql`
- `services/core/queries/roadmap_sync_steps.sql`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` present/read. 기본 local 검증은 standard remote runner `$HOME/agent-work/nomadcode`.
- Matched profile: `agent-test/local/core-smoke.md`.
- Applied commands: core 변경은 최소 `cd services/core && go test ./...`; 보조로 `go vet ./...` 가능. 원격 runner에서는 `ssh toki@toki-labs.com 'zsh -lc ...'` 형식을 사용한다.
- Fallback: 없음. 이번 분석 중 current workspace와 standard remote runner 모두 `cd services/core && go test ./...` 통과했다.

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

- `identity-write`: 현재 `BranchRevisionScanner.Scan`은 provider identity 없는 Milestone을 `ChangedFiles`에만 남기고 `Docs`에서 제외한다. Core가 push 전 identity block을 주입하는 테스트가 없다. 새 테스트 필요.
- Scheduler wiring: 현재 `TaskWorker.runTask`는 first authoring 성공 뒤 바로 push-only call을 보낸다. identity writer 호출과 실패 metadata 테스트가 없다. 새 테스트 필요.
- Gito scan regression: writer가 만든 identity block을 scanner가 parse해 `Docs`에 싣는 통합성 테스트가 없다. 새 테스트 필요.

### 심볼 참조

- Renamed/removed symbols: none.
- New symbols expected: `authoring.ProviderIdentityWriter` 또는 동등 인터페이스, `authoring.EnsureProviderIdentityInput`, `authoring.ErrProviderIdentityWriteFailed` 또는 더 구체적 error.
- Call sites to update: `services/core/internal/scheduler/jobs.go`, `services/core/internal/scheduler/river.go`, `services/core/cmd/server/main.go`, related tests.

### 분할 판단

- Split policy evaluated before file selection.
- Shared task group: `m-plane-origin-authoring-roundtrip-sync`.
- Current subtask: `04+03_identity_write`.
- Dependency: predecessor index `03` is satisfied by `agent-task/archive/2026/06/m-plane-origin-authoring-roundtrip-sync/03+01,02_push_command/complete.log`.
- Single active plan is appropriate for this subtask because the work is one runtime handoff: authoring output identity injection before push-only. It crosses authoring/scheduler/server wiring, but the change is one dependency chain and cannot produce useful PASS evidence until writer, wiring, and scanner regression are together.

### 범위 결정 근거

- Do not change authoring prompt text or add Plane/provider contract wording to prompt.
- Do not implement Plane projection, Todo move, live smoke, or Gito delivery changes here.
- Do not change DB schema unless implementation proves pre-push injection cannot satisfy S06. Existing ledger remains for idempotency.
- Do not archive Milestone or mark non-target tasks.

### 빌드 등급

- `cloud-G07`: filesystem/git workspace mutation plus scheduler/server wiring can pass narrow unit tests while breaking external authoring runtime if sequenced incorrectly.

## 구현 체크리스트

- [ ] [IDENTITY_WRITE-1] Core-side provider identity writer를 추가해 slot checkout의 changed Milestone 파일에 `## Provider identity` block을 결정적으로 주입하고 단위 테스트를 작성한다.
- [ ] [IDENTITY_WRITE-2] Scheduler/server wiring에서 first authoring 성공 후 push-only 전 writer를 호출하고, identity write 실패가 명시 failure metadata/slot state로 남도록 테스트한다.
- [ ] [IDENTITY_WRITE-3] Gito scanner regression과 최종 core 검증으로 identity 없는 generated Milestone이 조용히 drop되지 않음을 확인한다. 검증: `cd services/core && go test -count=1 ./internal/authoring/... ./internal/scheduler/... ./internal/gitosync/...` 및 `cd services/core && go test -count=1 ./...`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

`04+03_identity_write`는 predecessor `03+01,02_push_command`의 `complete.log`가 있어야 시작한다. 해당 archive complete log는 확인 완료다. 구현 순서는 writer -> scheduler/server wiring -> scanner regression 순서다.

### [IDENTITY_WRITE-1] Add Core Identity Writer

문제: `BuildAuthoringGenerateInputStrict`는 provider/work item identity를 `WorkspaceMetadata`에 보존하지만, IOP `/v1/responses` wire metadata는 workspace path만 보내고 Milestone 파일에는 identity block을 쓰지 않는다. `services/core/internal/scheduler/jobs.go:200` 이후 first authoring call이 끝나도 slot checkout 파일을 보강하는 단계가 없다.

Before:

```go
// services/core/internal/scheduler/jobs.go:200
generated, err := w.Model.Generate(ctx, generateInput)
if err != nil {
	return nil, "", err
}

// Push-only second stage...
pushInput, _, pushInputErr := authoring.BuildAuthoringPushInputStrict(task)
```

해결 방법: `services/core/internal/authoring/identity_write.go`를 추가한다. `EnsureProviderIdentityInput{WorkspacePath, SourceBranch, Identity}`를 받아 changed milestone paths를 찾고, 각 Markdown에 `roadmapsync.ParseMilestoneIdentity`가 성공하면 그대로 둔다. `ErrNoProviderIdentity`이면 파일 끝에 표준 block을 append한다. path가 없거나 identity가 불완전하면 typed error를 반환한다.

After sketch:

```go
type IdentityWriter interface {
	EnsureProviderIdentity(ctx context.Context, in EnsureProviderIdentityInput) (EnsureProviderIdentityResult, error)
}

type EnsureProviderIdentityInput struct {
	WorkspacePath string
	Identity      roadmapsync.Identity
}
```

수정 파일 및 체크리스트:

- [ ] `services/core/internal/authoring/identity_write.go` 추가.
- [ ] `services/core/internal/authoring/identity_write_test.go` 추가.
- [ ] `roadmapsync.ParseMilestoneIdentity`를 재사용하고 ad hoc parser를 새로 만들지 않는다.
- [ ] identity block 필드는 provider, tenant, project, work item id, milestone id/roadmap item id 중 가능한 값을 deterministic order로 쓴다.

테스트 작성: `TestEnsureProviderIdentityInjectsMissingBlock`, `TestEnsureProviderIdentityKeepsExistingBlock`, `TestEnsureProviderIdentityFailsWhenNoMilestoneChanged`, `TestEnsureProviderIdentityRejectsMissingWorkItemID`.

중간 검증:

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

### [IDENTITY_WRITE-2] Wire Before Push

문제: `TaskWorker.runTask`는 first authoring success 뒤 바로 push-only request를 보낸다. identity write 실패가 push failure와 구분되는 metadata로 남지 않는다.

Before:

```go
// services/core/internal/scheduler/jobs.go:205
// Push-only second stage: send the authored content to the workspace
// agent so it can push to the remote develop branch before we gate on
// the develop match confirmation.
pushInput, _, pushInputErr := authoring.BuildAuthoringPushInputStrict(task)
```

해결 방법: `TaskWorker`에 `IdentityWriter authoring.IdentityWriter` 필드를 추가하고 `scheduler.New`와 `cmd/server/main.go`에서 production writer를 주입한다. `generated` 성공 후 `pushInput` 생성 전 writer를 호출한다. writer error는 `authoring_failure_category=identity_missing` 또는 `identity_write_failed`로 분류하고 slot은 `error`로 수렴시킨다. nil writer는 명시적으로 error로 처리하거나 production에서 항상 주입하되 tests에서 fake/noop을 넣는다. 선택은 구현 중 하나로 고정하고 test로 보장한다.

After sketch:

```go
writeResult, err := w.IdentityWriter.EnsureProviderIdentity(ctx, authoring.EnsureProviderIdentityInput{
	WorkspacePath: generateInput.WorkspaceMetadata.Path,
	Identity:      identityFromWorkspaceMetadata(generateInput.WorkspaceMetadata),
})
if err != nil {
	return nil, "", authoringIdentityError{err: err}
}
```

수정 파일 및 체크리스트:

- [ ] `services/core/internal/scheduler/jobs.go`에 writer call과 error wrapper 추가.
- [ ] `services/core/internal/scheduler/river.go` 생성자 인자와 `TaskWorker` wiring 갱신.
- [ ] `services/core/cmd/server/main.go`에서 production writer 생성 후 scheduler에 전달.
- [ ] `services/core/internal/authoring/result.go`에 identity write failure category를 추가하거나 scheduler failure mapping에서 명시 category를 채운다.
- [ ] 기존 scheduler.New 호출 테스트를 모두 갱신한다.

테스트 작성: `TestWorkAuthoringWritesIdentityBeforePush`, `TestWorkAuthoringIdentityWriteFailureMarksIdentityMissing`, `TestSchedulerRegistersTaskWorkerWithIdentityWriter`.

중간 검증:

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

### [IDENTITY_WRITE-3] Scanner Regression And Final Verification

문제: `services/core/internal/gitosync/scanner.go:139`에서 provider identity parse 실패 시 `Docs`에 싣지 않고 continue한다. `services/core/internal/gitosync/bridge.go:119`는 `out.Docs`만 enqueue하고 `:127`에서 revision을 processed로 표시한다. writer가 없거나 실패했는데 push되면 projection이 조용히 사라질 수 있다.

Before:

```go
// services/core/internal/gitosync/scanner.go:139
identity, err := roadmapsync.ParseMilestoneIdentity(path, markdown)
if err != nil {
	// No provider identity block...
	continue
}
```

해결 방법: writer unit test fixture를 scanner test와 연결해, identity 없는 authored Markdown이 writer 이후 scanner에서 `ScannedMilestones`와 `Docs`로 들어오는 regression을 추가한다. bridge 자체는 identity 없는 docs를 projection하지 않는 현 gate를 유지한다. 필요하면 `ScanOutput`에 identity miss count/path를 추가해 log만 남기되, provider side effect는 하지 않는다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/gitosync/scanner_test.go`에 writer output 기반 parse regression 추가 또는 equivalent fixture 추가.
- [ ] `services/core/internal/gitosync/bridge_test.go`에 no-doc revision 처리 기대를 명확히 고정한다.
- [ ] Roadmap/Spec target 완료 근거를 review stub에 남길 수 있게 검증 출력 준비.

테스트 작성: scanner regression test required. Bridge behavior test는 변경 범위에 따라 작성 또는 기존 `TestScanDropsMilestoneWithoutProviderIdentity` 기대를 보강한다.

중간 검증:

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/authoring/identity_write.go` | IDENTITY_WRITE-1 |
| `services/core/internal/authoring/identity_write_test.go` | IDENTITY_WRITE-1 |
| `services/core/internal/authoring/result.go` | IDENTITY_WRITE-2 |
| `services/core/internal/authoring/result_test.go` | IDENTITY_WRITE-2 |
| `services/core/internal/scheduler/jobs.go` | IDENTITY_WRITE-2 |
| `services/core/internal/scheduler/jobs_test.go` | IDENTITY_WRITE-2 |
| `services/core/internal/scheduler/river.go` | IDENTITY_WRITE-2 |
| `services/core/internal/scheduler/river_test.go` | IDENTITY_WRITE-2 |
| `services/core/cmd/server/main.go` | IDENTITY_WRITE-2 |
| `services/core/cmd/server/main_test.go` | IDENTITY_WRITE-2 |
| `services/core/internal/gitosync/scanner_test.go` | IDENTITY_WRITE-3 |
| `services/core/internal/gitosync/bridge_test.go` | IDENTITY_WRITE-3 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/authoring/... ./internal/scheduler/... ./internal/gitosync/... ./internal/roadmapsync/...
cd services/core && go test -count=1 ./...
ssh toki@toki-labs.com 'zsh -lc '"'"'cd "$HOME/agent-work/nomadcode/services/core" && go test ./...'"'"''
git diff --check
```

모든 명령은 exit code 0이어야 한다. Go test cache는 focused/local 검증에서는 `-count=1`로 fresh 실행하고, standard remote runner baseline은 agent-test 규칙 명령 그대로 `go test ./...`를 허용한다.

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