nomadcode/agent-task/m-plane-work-item-webhook-intake/02+01_trigger_dispatch/PLAN-local-G07.md
toki f23ba78eba feat: plane webhook intake & protosocket consumer wire readiness
- Add Plane webhook handler for issue events (created, state, assignees)
- Add Plane webhook integration tests with testdata fixtures
- Add Gito Protosocket consumer wire readiness milestone
- Add Plane work item webhook intake milestone
- Add agent-task for plane-work-item-webhook-intake (trigger dispatch, idempotency, live smoke)
- Update service config, router, handlers for Plane webhook endpoints
- Add SOPS env setup script and secrets configuration
- Update agent-ops domain rules and phase roadmap
2026-06-14 20:37:48 +09:00

12 KiB

Plan - WEBHOOK_DISPATCH

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

구현 완료 전 active CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 반드시 채운다. 구현 중 사용자만 결정할 수 있는 외부 환경, secret, scope 충돌이 생기면 리뷰 stub의 사용자 리뷰 요청 섹션에 증거와 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 request_user_input을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

배경

Plane webhook receiver는 현재 인증된 요청을 받지만 creation pipeline을 호출하지 않는다. 기존 HTTP task creation route는 workitempipeline.CreateTaskFromWorkItem에 trigger state/assignee/self actor를 전달하는 경로가 이미 있다. 이 작업은 normalize 결과를 그 경로에 연결하되, ignored event는 workspace provision/slot reservation 없이 202로 끝나게 한다.

사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 직접 사용자 프롬프트는 금지하며, code-review가 USER_REVIEW 작성 여부를 판단한다.

Roadmap Targets

  • Milestone: agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-work-item-webhook-intake.md
  • Task ids:
    • trigger-dispatch: Backlog + AGENT assignee 이벤트만 기존 workitempipeline.CreateTaskFromWorkItem 경로로 넘기고 나머지는 side effect 없이 ignored 처리한다.
  • Completion mode: check-on-pass

분석 결과

읽은 파일

  • agent-test/local/rules.md
  • agent-test/local/core-smoke.md
  • agent-test/local/contracts-smoke.md
  • agent-test/plane-dev.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-work-item-webhook-intake.md
  • agent-ops/rules/project/domain/core/rules.md
  • services/core/internal/http/plane_webhook.go
  • services/core/internal/http/plane_webhook_test.go
  • services/core/internal/http/handlers.go
  • services/core/internal/http/handlers_test.go
  • services/core/internal/config/config.go
  • services/core/internal/config/config_test.go
  • services/core/cmd/server/main.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

테스트 환경 규칙

  • test_env=local로 적용한다.
  • agent-test/local/rules.mdagent-test/local/core-smoke.md를 읽었다.
  • core 변경이므로 최종 검증은 원격 기본 checkout에서 zsh -lc "cd ~/agent-work/nomadcode/services/core && go test ./..."를 실행한다.
  • contracts 파일은 수정하지 않으므로 contracts smoke는 적용하지 않는다.
  • <확인 필요> 값은 없다.

테스트 커버리지 공백

  • services/core/internal/http/handlers_test.go:738는 provider-neutral task route가 trigger를 전달하는지만 확인한다.
  • services/core/internal/workitempipeline/service_test.go:456은 wrong state/missing assignee ignored 시 side effect가 없음을 확인한다.
  • ReceivePlaneWebhook에서 normalize 결과를 workItemProviders["plane"]로 dispatch하는 테스트는 없다.
  • config에는 PLANE_WEBHOOK_SECRET만 있고 Backlog state id, AGENT assignee id, self actor id 설정은 없다.

심볼 참조

  • renamed/removed symbol 없음.

분할 판단

  • split decision policy를 먼저 평가했다.
  • shared task group: m-plane-work-item-webhook-intake
  • 이 subtask directory 02+01_trigger_dispatch는 predecessor 01을 요구한다.
  • predecessor 01 상태: agent-task/m-plane-work-item-webhook-intake/01_payload_normalization/complete.log 없음. 구현 시작 전 01_payload_normalization PASS/archive 또는 active complete.log가 필요하다.

범위 결정 근거

  • 이 작업은 webhook-to-pipeline dispatch와 설정 주입까지만 다룬다.
  • payload normalize helper는 01_payload_normalization의 산출물을 사용한다.
  • DB unique constraint, duplicate delivery, retry idempotency는 03+02_idempotency에서 다룬다.
  • remote live smoke와 Plane UI evidence는 04+01,02,03_live_smoke에서 다룬다.

빌드 등급

  • local-G07: HTTP handler, config, server wiring, pipeline call boundary를 함께 만지지만 기존 trigger gate와 focused handler tests로 검증 가능하다.

의존 관계 및 구현 순서

  • 01_payload_normalizationcomplete.log를 생성한 뒤 시작한다.
  • 이 plan은 directory name의 predecessor +01만 의존한다. 다른 숨은 의존성을 추가하지 않는다.

구현 체크리스트

  • Plane webhook dispatch 설정을 config/server/handler에 추가하고 PLANE_CREATION_BACKLOG_STATE_ID, PLANE_AGENT_ASSIGNEE_ID, optional PLANE_SELF_ACTOR_ID env를 주입한다.
  • ReceivePlaneWebhook에서 normalized Plane issue event만 workitempipeline.CreateTaskFromWorkItem으로 넘기고 ignored event는 side effect 없이 202로 반환한다. 검증: ignored event가 workspace provision 또는 slot reservation을 발생시키지 않는다.
  • handler/config tests를 추가해 matching event는 trigger input으로 전달되고 non-matching/ignored event는 task creator를 호출하지 않음을 확인한다.
  • CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

[WEBHOOK_DISPATCH-1] Dispatch Configuration

문제

services/core/internal/config/config.go:28에는 Plane base/token/webhook secret만 있고, trigger gate에 필요한 Backlog state id와 AGENT assignee id 설정이 없다. services/core/cmd/server/main.go:146은 secret만 handler에 주입한다.

해결 방법

Before:

// services/core/internal/config/config.go:28
PlaneBaseURL       string
PlaneToken         string
PlaneWebhookSecret string

After:

PlaneBaseURL                    string
PlaneToken                      string
PlaneWebhookSecret              string
PlaneCreationBacklogStateID     string
PlaneAgentAssigneeID            string
PlaneSelfActorID                string

Add env loading:

PlaneCreationBacklogStateID: firstEnv("PLANE_CREATION_BACKLOG_STATE_ID", "PLANE_BACKLOG_STATE_ID"),
PlaneAgentAssigneeID:        os.Getenv("PLANE_AGENT_ASSIGNEE_ID"),
PlaneSelfActorID:            os.Getenv("PLANE_SELF_ACTOR_ID"),

Wire a handler method such as SetPlaneWebhookDispatchConfig.

수정 파일 및 체크리스트

  • services/core/internal/config/config.go: add fields/env loading.
  • services/core/internal/config/config_test.go: add env test.
  • services/core/cmd/server/main.go: pass config to handler.
  • services/core/docker-compose.yml: expose non-secret ids with defaults.
  • agent-test/plane-dev.md: document non-secret env values using existing Backlog and AGENT ids.

테스트 작성

  • Required config test:
    • TestConfigLoadsPlaneWebhookDispatchEnv

중간 검증

cd services/core && go test -count=1 ./internal/config

Expected: package passes.

[WEBHOOK_DISPATCH-2] Handler Dispatch Path

문제

services/core/internal/http/plane_webhook.go:60 unmarshals payload and :84 returns accepted. It does not use Handler.workItemProviders from services/core/internal/http/handlers.go:32, so a valid Plane event cannot start the existing creation sync path.

해결 방법

Before:

// services/core/internal/http/plane_webhook.go:81
h.logger.Info("plane webhook received", attrs...)
writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "accepted"})

After:

event, ok, err := normalizePlaneWebhookPayload(payload)
if err != nil { writeError(w, stdhttp.StatusBadRequest, "invalid plane webhook payload"); return }
if !ok { writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status":"ignored"}); return }

task, err := h.dispatchPlaneWebhookWorkItem(r.Context(), event)
if errors.Is(err, workitempipeline.ErrCreationTriggerIgnored) {
	writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status":"ignored"})
	return
}
if err != nil { h.writeServiceError(w, err); return }
writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status":"accepted","task_id":task.ID})

Dispatch input must set:

Trigger: workitempipeline.CreationTrigger{
	RequiredStateID:    h.planeWebhookDispatch.BacklogStateID,
	RequiredAssigneeID: h.planeWebhookDispatch.AgentAssigneeID,
	Actor:              event.ActorID,
	SelfActor:          h.planeWebhookDispatch.SelfActorID,
}

수정 파일 및 체크리스트

  • services/core/internal/http/handlers.go: add dispatch config field.
  • services/core/internal/http/plane_webhook.go: add dispatch helper and error mapping.
  • Ensure missing dispatch config yields 202 ignored or 503 only by explicit choice recorded in review. Prefer 202 ignored when receiver is reachable but trigger config absent to avoid provider retry storms.

테스트 작성

  • Required handler tests:
    • TestReceivePlaneWebhookDispatchesMatchingIssueEvent
    • TestReceivePlaneWebhookIgnoresIrrelevantEventWithoutCreatorCall
    • TestReceivePlaneWebhookReturnsAcceptedWhenCreationTriggerIgnored

중간 검증

cd services/core && go test -count=1 ./internal/http

Expected: package passes.

[WEBHOOK_DISPATCH-3] Side Effect Guard Verification

문제

services/core/internal/workitempipeline/service.go:103 fetches item and :109 evaluates trigger before provision/slot reservation at :113, but webhook handler tests do not prove ignored webhook events avoid dispatch or map ignored trigger to 202.

해결 방법

Keep side-effect guard inside workitempipeline; handler should only pass strict trigger config and map ErrCreationTriggerIgnored to accepted/ignored. Do not duplicate Plane current-state checks in webhook payload logic.

수정 파일 및 체크리스트

  • services/core/internal/http/plane_webhook_test.go: assert fake creator call counts.
  • services/core/internal/workitempipeline/service_test.go: only add tests if dispatch requires new trigger semantics; otherwise rely on existing TestCreateTaskFromWorkItemIgnoresNonMatchingTrigger.

테스트 작성

  • Handler tests required.
  • Workitempipeline tests can be skipped if no pipeline behavior changes; record skip reason in review.

중간 검증

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

Expected: packages pass.

수정 파일 요약

파일 항목
services/core/internal/config/config.go WEBHOOK_DISPATCH-1
services/core/internal/config/config_test.go WEBHOOK_DISPATCH-1
services/core/cmd/server/main.go WEBHOOK_DISPATCH-1
services/core/docker-compose.yml WEBHOOK_DISPATCH-1
agent-test/plane-dev.md WEBHOOK_DISPATCH-1
services/core/internal/http/handlers.go WEBHOOK_DISPATCH-2
services/core/internal/http/plane_webhook.go WEBHOOK_DISPATCH-2
services/core/internal/http/plane_webhook_test.go WEBHOOK_DISPATCH-2, WEBHOOK_DISPATCH-3
services/core/internal/workitempipeline/service_test.go WEBHOOK_DISPATCH-3, optional

최종 검증

cd services/core && gofmt -w internal/config/config.go internal/config/config_test.go internal/http/handlers.go internal/http/plane_webhook.go internal/http/plane_webhook_test.go cmd/server/main.go
cd services/core && go test -count=1 ./internal/config ./internal/http ./internal/workitempipeline
tar -C services/core -czf - . | ssh toki@toki-labs.com 'mkdir -p ~/agent-work/nomadcode/services/core && tar -C ~/agent-work/nomadcode/services/core -xzf -'
ssh toki@toki-labs.com 'zsh -lc "cd ~/agent-work/nomadcode/services/core && go test -count=1 ./..."'

Expected: local focused tests pass and remote full core tests pass.

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