- 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
13 KiB
Plan - WEBHOOK_IDEMPOTENCY
이 파일을 읽는 구현 에이전트에게
구현 완료 전 active CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 반드시 채운다. 구현 중 사용자만 결정할 수 있는 외부 환경, secret, scope 충돌이 생기면 리뷰 stub의 사용자 리뷰 요청 섹션에 증거와 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 request_user_input을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
배경
Plane은 webhook retry나 duplicate delivery를 보낼 수 있고, NomadCode self mutation event도 다시 들어올 수 있다. 현재 task insert는 external provider/id를 저장하지만 unique/idempotent guard가 없다. 이 작업은 같은 Plane work item이 여러 번 들어와도 creation task와 authoring run이 중복되지 않도록 저장소 경계를 고정한다.
사용자 리뷰 요청 흐름
구현 중 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:
loop-idempotency: duplicate webhook, provider retry, NomadCode self actor event를 중복 creation task 없이 처리한다.
- Completion mode: check-on-pass
분석 결과
읽은 파일
agent-test/local/rules.mdagent-test/local/core-smoke.mdagent-test/local/contracts-smoke.mdagent-test/plane-dev.mdagent-roadmap/current.mdagent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.mdagent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-work-item-webhook-intake.mdagent-ops/rules/project/domain/core/rules.mdservices/core/internal/storage/store.goservices/core/internal/workflow/service.goservices/core/internal/workflow/service_test.goservices/core/internal/db/tasks.sql.goservices/core/queries/tasks.sqlservices/core/migrations/00001_create_tasks.sqlservices/core/migrations/00002_add_task_external_refs.sqlservices/core/sqlc.yamlservices/core/internal/workitempipeline/service.goservices/core/internal/workitempipeline/service_test.goservices/core/internal/http/plane_webhook.goservices/core/internal/http/plane_webhook_test.go
테스트 환경 규칙
test_env=local로 적용한다.agent-test/local/rules.md와agent-test/local/core-smoke.md를 읽었다.- core storage/query/generated code 변경이므로 원격 기본 checkout에서
zsh -lc "cd ~/agent-work/nomadcode/services/core && go test ./..."를 실행한다. - 로컬에
sqlcbinary는 없었다. generated db code가 필요하면cd services/core && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate를 사용한다. <확인 필요>값은 없다.
테스트 커버리지 공백
services/core/queries/tasks.sql:1의CreateTask는 plain insert라 duplicate external ref를 막지 않는다.services/core/migrations/00002_add_task_external_refs.sql:2는 external columns만 추가하고 unique index가 없다.services/core/internal/workflow/service.go:39는 normalized external ref를 저장소로 넘기지만 idempotent result semantics를 정의하지 않는다.services/core/internal/workitempipeline/service_test.go:522는 self actor ignored가 side effect를 만들지 않음을 확인하지만 duplicate webhook retry는 확인하지 않는다.
심볼 참조
- renamed/removed symbol 없음.
CreateTasksemantics 변경 또는 새 query 추가 시 call site는workflow.Service.CreateTask와workitempipeline.TaskCreator경계다.
분할 판단
- split decision policy를 먼저 평가했다.
- shared task group:
m-plane-work-item-webhook-intake - 이 subtask directory
03+02_idempotency는 predecessor02를 요구한다. - predecessor
02상태:agent-task/m-plane-work-item-webhook-intake/02+01_trigger_dispatch/complete.log없음. 구현 시작 전02+01_trigger_dispatchPASS/archive 또는 active complete.log가 필요하다.
범위 결정 근거
- 이 작업은 duplicate task prevention까지 다룬다.
- Payload parsing과 handler dispatch는 선행 task 산출물로 간주한다.
- UI/live smoke evidence는
04+01,02,03_live_smoke에서 다룬다. - Historical data backfill은 필요할 때 migration의 duplicate-precheck로만 다루고, 사용자 데이터 수동 정리는 이 plan 범위 밖이다. migration이 기존 duplicate로 실패한다면 review stub의 사용자 리뷰 요청에 정확한 SQL evidence를 남긴다.
빌드 등급
cloud-G06: DB unique constraint, generated SQL, workflow idempotent semantics가 얽혀 있어 저장소/protocol 판단 리스크가 있다.
의존 관계 및 구현 순서
02+01_trigger_dispatch가complete.log를 생성한 뒤 시작한다.- 이 plan은 directory name의 predecessor
+02만 의존한다.
구현 체크리스트
- Plane-origin task idempotency key를 external provider/id로 고정하고 DB unique/index 또는 upsert semantics를 추가한다.
- duplicate webhook/provider retry가 기존 task를 반환하거나 ignored 처리되어 새 task, workspace provision, slot reservation을 만들지 않도록 workflow/workitempipeline 경계를 조정한다. 검증: 같은 provider/work item/revision 재처리와 self actor 입력이 task 중복 생성으로 이어지지 않는다.
- storage/workflow/workitempipeline/http tests를 추가해 duplicate external ref, duplicate webhook dispatch, self actor ignored를 검증한다.
- CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
[WEBHOOK_IDEMPOTENCY-1] Storage Idempotency Boundary
문제
services/core/queries/tasks.sql:1은 plain insert이고 services/core/migrations/00002_add_task_external_refs.sql:2에는 unique index가 없다. 같은 external_provider=plane, external_id=<work item id>가 여러 번 들어오면 task row가 중복될 수 있다.
해결 방법
Before:
-- services/core/queries/tasks.sql:1
INSERT INTO tasks (title, source, payload, metadata, external_provider, external_id, external_url, external_metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, title, source, status, payload, result, error, created_at, updated_at, external_provider, external_id, external_url, external_metadata, metadata;
After:
CREATE UNIQUE INDEX IF NOT EXISTS ux_tasks_external_ref
ON tasks (external_provider, external_id)
WHERE external_provider IS NOT NULL AND external_id IS NOT NULL;
Use an upsert/no-op-returning query for external refs:
INSERT INTO tasks (...)
VALUES (...)
ON CONFLICT (external_provider, external_id)
WHERE external_provider IS NOT NULL AND external_id IS NOT NULL
DO UPDATE SET updated_at = tasks.updated_at
RETURNING ...;
If changing global CreateTask is too broad after reading current call sites, add a separate generated query and keep plain internal task creation unchanged. Record the choice in review.
수정 파일 및 체크리스트
services/core/migrations/00008_add_task_external_ref_unique.sql: add partial unique index.services/core/queries/tasks.sql: add idempotent create query or adjustCreateTask.services/core/internal/db/tasks.sql.go: regenerate with sqlc.services/core/internal/storage/store.go: expose idempotent create semantics.
테스트 작성
- Required storage/workflow tests.
- If no DB integration test harness exists, use generated SQL string assertions plus workflow fake store semantics, and record the limitation.
중간 검증
cd services/core && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
Expected: generated db files update cleanly.
[WEBHOOK_IDEMPOTENCY-2] Workflow And Pipeline Semantics
문제
services/core/internal/workflow/service.go:64 always calls store.CreateTask; services/core/internal/workitempipeline/service.go:113 provisions workspace and reserves slot before task insert. If duplicate detection happens only at insert time, duplicate webhook can still reserve a slot before returning existing task.
해결 방법
Prefer checking idempotency before workspace provision/slot reservation when external provider/id is present. Options:
- Add a narrow store lookup by external ref and let
workitempipelinereturn existing task before provision. - Add an idempotent reservation-safe
TaskCreatormethod only for work item creation.
Choose the smallest change that prevents duplicate slot reservation. Do not rely solely on post-provision insert conflict unless tests prove no duplicate side effects.
수정 파일 및 체크리스트
services/core/internal/workflow/service.go: add or route idempotent external-ref lookup/create semantics.services/core/internal/workitempipeline/service.go: avoid workspace/slot side effects for already-created work item task.services/core/internal/workitempipeline/service_test.go: assert duplicate path does not callEnsureProjectWorkspaceorReserveWorkspaceSlot.
테스트 작성
- Required:
TestCreateTaskFromWorkItemReturnsExistingExternalTaskWithoutProvisionTestCreateTaskFromWorkItemDuplicateExternalRefDoesNotReserveSlot
중간 검증
cd services/core && go test -count=1 ./internal/workflow ./internal/workitempipeline
Expected: packages pass.
[WEBHOOK_IDEMPOTENCY-3] Webhook Duplicate/Self Tests
문제
Self actor guard exists in services/core/internal/workitempipeline/service.go:162, but webhook handler must pass actor/self actor consistently. Duplicate webhook handler tests do not exist yet.
해결 방법
Add handler-level tests that call ReceivePlaneWebhook twice with same work item event. The fake creator should prove the second call does not create duplicate side effects once idempotency boundary exists. Keep self actor ignored returning 202.
수정 파일 및 체크리스트
services/core/internal/http/plane_webhook_test.go: duplicate signed event test.services/core/internal/http/plane_webhook_test.go: self actor ignored test if not covered by dispatch task.
테스트 작성
- Required because webhook retry is the milestone task's behavioral surface.
중간 검증
cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline
Expected: packages pass.
수정 파일 요약
| 파일 | 항목 |
|---|---|
services/core/migrations/00008_add_task_external_ref_unique.sql |
WEBHOOK_IDEMPOTENCY-1 |
services/core/queries/tasks.sql |
WEBHOOK_IDEMPOTENCY-1 |
services/core/internal/db/tasks.sql.go |
WEBHOOK_IDEMPOTENCY-1 |
services/core/internal/storage/store.go |
WEBHOOK_IDEMPOTENCY-1 |
services/core/internal/workflow/service.go |
WEBHOOK_IDEMPOTENCY-2 |
services/core/internal/workflow/service_test.go |
WEBHOOK_IDEMPOTENCY-1, WEBHOOK_IDEMPOTENCY-2 |
services/core/internal/workitempipeline/service.go |
WEBHOOK_IDEMPOTENCY-2 |
services/core/internal/workitempipeline/service_test.go |
WEBHOOK_IDEMPOTENCY-2 |
services/core/internal/http/plane_webhook_test.go |
WEBHOOK_IDEMPOTENCY-3 |
최종 검증
cd services/core && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
cd services/core && gofmt -w internal/db/tasks.sql.go internal/storage/store.go internal/workflow/service.go internal/workflow/service_test.go internal/workitempipeline/service.go internal/workitempipeline/service_test.go internal/http/plane_webhook_test.go
cd services/core && go test -count=1 ./internal/storage ./internal/workflow ./internal/workitempipeline ./internal/http
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: sqlc generation succeeds, focused tests pass, remote full core tests pass.
모든 코드 변경 완료 후 반드시 CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.