<!-- task=m-milestone-work-item-creation-sync/09_creation_trigger_gate plan=0 tag=API -->

# Plane Creation Trigger Gate

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

`CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 구현 후 검증 명령의 실제 stdout/stderr를 붙이고 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 종결, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 범위 충돌이 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정, 근거, 실행 명령/출력, 재개 조건을 적고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 재실행/증거 수집으로 닫을 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.

## 배경

`creation-sync` Epic의 앞단은 Plane `Backlog + AGENT assignee` 상위 티켓만 Milestone 생성 trigger로 받아야 한다. 현재 HTTP/pipeline 경로는 provider work item id만 받으면 task를 만들고, fetched work item의 state/assignee 또는 NomadCode self mutation 여부를 gate하지 않는다. 이 계획은 기존 project sync/slot 예약 흐름은 유지하면서 trigger eligibility와 no-op 결과를 provider-neutral하게 추가한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 사용자에게 묻지 않으며, 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:
  - `plane-trigger`: Plane Backlog + AGENT assignee trigger gate
- 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-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/workspace-ops/rules.md`
- `agent-ops/rules/project/domain/core/rules.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-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `services/core/internal/workitem/provider.go`
- `services/core/internal/workitem/provider_test.go`
- `services/core/internal/workitempipeline/service.go`
- `services/core/internal/workitempipeline/service_test.go`
- `services/core/internal/adapters/plane/client.go`
- `services/core/internal/adapters/plane/client_test.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/authoring/request.go`
- `services/core/internal/authoring/request_test.go`
- `services/core/internal/roadmapsync/retry.go`
- `services/core/internal/roadmapsync/retry_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`

### 테스트 환경 규칙

`test_env=local`. `agent-test/local/rules.md`를 읽었고 core 변경은 `agent-test/local/core-smoke.md`, roadmap/task artifact 변경은 `agent-test/local/workspace-ops-smoke.md`를 적용한다. contracts 파일은 읽기만 했고 수정 대상이 아니므로 `contracts-smoke`는 영향 검토용이다. 필수 검증은 core focused `go test -count=1`과 `git diff --check`다. local 규칙의 remote runner 표준은 남아 있으나 이 계획은 repo-local 명령으로 검증 계약을 고정한다.

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

- Trigger state/assignee eligibility: 기존 테스트 없음. `workitempipeline`와 `http` 테스트 추가 필요.
- Plane fetched state/assignee mapping: 기존 테스트 없음. `adapters/plane` 테스트 추가 필요.
- NomadCode self mutation no-op: `roadmapsync.ReconcileCreationCycle` 단위 테스트는 있으나 intake gate 연결 테스트 없음. pipeline 또는 HTTP 테스트 추가 필요.

### 심볼 참조

새 symbol 추가 중심이며 rename/remove 없음. 기존 연결점: `workitem.WorkItem.State`, `workitem.WorkItem.AssigneeIDs`, `workitempipeline.CreateTaskInput`, `Handler.createWorkItemTask`, `plane.Client.FetchWorkItem`, `roadmapsync.ReconcileCreationCycle`.

### 분할 판단

split decision policy를 먼저 평가했다. shared task group은 `m-milestone-work-item-creation-sync`다.

- `09_creation_trigger_gate`: trigger eligibility, actor guard 연결, HTTP no-op semantics. 선행 의존성 없음.
- `10+09_plane_draft_authoring`: authoring draft/idempotency contract. predecessor `09`는 현재 active/archive `complete.log`가 없으므로 implementation은 `09_creation_trigger_gate` PASS 후 시작해야 한다.

이 plan은 `09`만 구현한다. `10`은 별도 active plan이므로 이 파일에서 구현하지 않는다.

### 범위 결정 근거

Plane native webhook signature 검증, polling, Plane custom property, Todo 이후 In Progress 실행 lifecycle은 마일스톤 범위 밖이다. 이번 변경은 existing HTTP intake와 provider-neutral pipeline에 eligibility gate를 추가하는 데 한정한다. AGENT assignee id는 secret이 아니지만 project별 runtime 설정일 수 있으므로 hard-code하지 않고 request/config에서 주입 가능한 입력으로 둔다.

### 빌드 등급

`local-G06`: core HTTP/pipeline/adapter 경계를 건드리지만 파일 수와 테스트 범위가 제한적이고 결정적 단위 테스트로 검증 가능하다.

## 구현 체크리스트

- [ ] provider-neutral trigger criteria와 self actor no-op 결정을 추가하고 `workitempipeline.CreateTaskFromWorkItem`에서 task 생성 전 적용한다. 검증: NomadCode self mutation과 state/assignee 불일치가 task 생성 없이 typed no-op으로 끝난다.
- [ ] Plane adapter가 fetched work item의 state와 assignee id 후보를 `workitem.WorkItem`에 매핑하도록 보강한다.
- [ ] HTTP Plane/generic work item task request가 trigger state, AGENT assignee, actor/self actor 입력을 pipeline으로 전달하고 ignored trigger를 202 no-op 응답으로 반환한다.
- [ ] focused Go tests와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Pipeline Trigger Decision

문제: [service.go](/config/workspace/nomadcode/services/core/internal/workitempipeline/service.go:109)는 fetched work item을 받은 뒤 바로 task input을 만들고 [service.go](/config/workspace/nomadcode/services/core/internal/workitempipeline/service.go:125)에서 task를 생성한다. `workitem.WorkItem`에는 state/assignee 필드가 이미 있지만 [provider.go](/config/workspace/nomadcode/services/core/internal/workitem/provider.go:74), gate가 없다.

Before:

```go
// services/core/internal/workitempipeline/service.go:109
item, err := s.reader.FetchWorkItem(ctx, input.Ref)
if err != nil {
	return storage.Task{}, err
}
createInput, err := workitem.BuildCreateTaskInput(workitem.TaskCreateInput{
	Ref:     input.Ref,
	StateID: input.StateID,
	Comment: input.Comment,
}, item)
```

해결 방법: `workitempipeline.CreateTaskInput`에 `Trigger CreationTrigger`를 추가한다. `CreationTrigger`는 `RequiredStateID`, `RequiredAssigneeID`, `Actor`, `SelfActor`를 가진다. `EvaluateCreationTrigger(item, trigger)` helper를 추가해 criteria가 비어 있으면 기존 호환 경로처럼 accept하고, `Actor == SelfActor` 또는 state/assignee 불일치면 `ErrCreationTriggerIgnored`를 반환한다. `CreateTaskFromWorkItem`은 `FetchWorkItem` 후, `BuildCreateTaskInput` 전 gate를 호출한다. self actor guard는 `roadmapsync.ReconcileCreationCycle`과 같은 의미의 reason string을 쓰되, pipeline helper가 `roadmapsync`를 import해 cycle domain을 거꾸로 당기지 않게 한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/workitempipeline/service.go`: trigger input type, typed error, gate helper, `CreateTaskFromWorkItem` 적용.
- [ ] `services/core/internal/workitempipeline/service_test.go`: accepted, wrong state ignored, missing AGENT assignee ignored, self actor ignored, empty trigger compatibility 테스트.
- [ ] `services/core/internal/workitem/provider.go`: 필요하면 assignee matching helper만 추가하고 provider-neutral DTO 의미를 보강한다.
- [ ] `services/core/internal/workitem/provider_test.go`: helper 추가 시 trim/contains 테스트.

테스트 작성: `TestCreateTaskFromWorkItemIgnoresNonMatchingTrigger`, `TestCreateTaskFromWorkItemIgnoresSelfActor`, `TestCreateTaskFromWorkItemEmptyTriggerKeepsCompatibility`를 추가한다.

중간 검증:

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

### [API-2] Plane And HTTP Trigger Inputs

문제: Plane compatibility route와 generic route는 body에서 provider ref만 만들며 [handlers.go](/config/workspace/nomadcode/services/core/internal/http/handlers.go:284) request shape에 trigger fields가 없다. Plane adapter `FetchWorkItem`도 현재 title/body만 neutral item으로 옮겨 trigger gate가 볼 state/assignee evidence를 제공하지 않는다.

Before:

```go
// services/core/internal/http/handlers.go:284
type createWorkItemTaskRequest struct {
	Tenant      string `json:"tenant"`
	Project     string `json:"project"`
	ID          string `json:"id"`
	Provider    string `json:"provider,omitempty"`
	ExternalURL string `json:"external_url"`
	StateID     string `json:"state_id"`
	Comment     string `json:"comment"`
}
```

해결 방법: request에 `trigger_state_id`, `agent_assignee_id`, `actor`, `self_actor`를 추가한다. legacy Plane route에도 같은 optional fields를 추가한다. `createWorkItemTask`가 `workitempipeline.CreationTrigger`를 채워 전달한다. `ErrCreationTriggerIgnored`는 HTTP에서 `202 Accepted`와 `{"status":"ignored","reason":...}`로 반환해 webhook/retry caller가 실패로 보지 않게 한다. Plane adapter는 response의 `state`/`state_id`와 `assignees`/`assignee_ids` 후보를 문자열 id 목록으로 수집해 `workitem.WorkItem.State`와 `AssigneeIDs`에 채운다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/http/handlers.go`: request fields, trigger mapping, 202 ignored response.
- [ ] `services/core/internal/http/handlers_test.go`: Plane/generic route trigger 전달, ignored trigger 202 응답.
- [ ] `services/core/internal/adapters/plane/client.go`: fetched state/assignee mapping.
- [ ] `services/core/internal/adapters/plane/client_test.go`: state and assignee id mapping 테스트.

테스트 작성: `TestCreateWorkItemTaskPassesCreationTrigger`, `TestCreateWorkItemTaskReturns202WhenTriggerIgnored`, `TestFetchWorkItemMapsStateAndAssignees`.

중간 검증:

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/workitempipeline/service.go` | API-1 |
| `services/core/internal/workitempipeline/service_test.go` | API-1 |
| `services/core/internal/workitem/provider.go` | API-1 |
| `services/core/internal/workitem/provider_test.go` | API-1 |
| `services/core/internal/http/handlers.go` | API-2 |
| `services/core/internal/http/handlers_test.go` | API-2 |
| `services/core/internal/adapters/plane/client.go` | API-2 |
| `services/core/internal/adapters/plane/client_test.go` | API-2 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/workitem ./internal/workitempipeline ./internal/adapters/plane ./internal/http
git diff --check
```

기대 결과: 모든 package가 `ok`로 종료하고 `git diff --check` 출력이 없다. 캐시 출력은 허용하지 않으며 `-count=1`로 fresh 실행한다.

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