diff --git a/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_0.log b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_0.log new file mode 100644 index 0000000..b19485f --- /dev/null +++ b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_0.log @@ -0,0 +1,171 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only. + +## 개요 + +date=2026-06-15 +task=m-milestone-work-item-creation-sync/12+11_authoring_develop_gate, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md` +- Task ids: + - `authoring-develop-gate`: IOP authoring 응답만으로 task 완료를 단정하지 않고, `develop` Milestone push와 provider identity match가 확인될 때 authoring 성공 또는 sync-ready로 판정된다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/`로 이동한다. +4. PASS이고 task group이 `m-milestone-work-item-creation-sync`이면 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Authoring model response must not complete the task | [x] | +| [API-2] Creation sync projection finalizes the originating task | [x] | + +## 구현 체크리스트 + +- [x] Plane-origin authoring task가 model response-only 상태에서 `completed`로 전이되지 않고 `running` + `authoring_run_state=in_progress` + wait reason을 유지하도록 scheduler/workflow 경계를 수정한다. +- [x] `RoadmapCreationSyncWorker`가 `SyncActionProjected` 또는 already-complete outcome에서 원래 Plane external ref task를 완료할 수 있는 finalizer seam을 추가한다. +- [x] model bridge error, push failure, dirty workspace, conflict 같은 failure category는 task failure metadata로 남기고 Plane `Todo` projection으로 넘어가지 않도록 테스트한다. +- [x] `cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync`를 통과시킨다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_0.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리를 archive로 이동한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +- `workflow.Service`와 `scheduler.Client` 간의 순환 참조를 해결하기 위해 `workflow.Service`에 `SetEnqueuer(enqueuer TaskEnqueuer)` 메서드를 새로 추가하고, `main.go`에서 지연 바인딩(lazy wiring) 방식을 통해 생성 및 주입 순서를 조정하였습니다. +- `CompleteByExternalRef` 메서드명을 가이드라인에 맞게 `CompleteTaskByExternalRef`로 일원화하여 인터페이스 호환을 마쳤습니다. + +## 주요 설계 결정 + +- `RoadmapCreationSyncWorker` 내부에서 사용할 `TaskFinalizer` 인터페이스를 `CompleteTaskByExternalRef` 메서드명을 갖도록 설계하여 `workflow.Service`가 이를 직접적으로 호환하도록 만들었습니다. +- `Workflow` 서비스의 `CompleteTaskByExternalRef`는 concurrency-safe 하도록 기존 `WithExternalRefLock` 뮤텍스 락 메커니즘을 그대로 활용하도록 설계했습니다. 또한 이미 `completed` 상태인 경우에는 중복 처리를 방지하기 위해 no-op으로 즉시 반환하도록 설계했습니다. + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- model response-only authoring task가 `completed`로 전이되지 않는지 확인한다. +- projection terminal outcome만 원래 external-ref task를 완료하는지 확인한다. +- not_ready/conflict/self-mutation outcome에서 provider side effect와 task completion이 섞이지 않는지 확인한다. + +## 검증 결과 + +### API-1 중간 검증 +```text +$ cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.015s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.004s +``` + +### API-2 중간 검증 +```text +$ cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow ./internal/roadmapsyncpipeline +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.010s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.003s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.004s +``` + +### 최종 검증 +```text +$ cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.012s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.006s +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.004s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.004s +ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.006s + +$ cd services/core && go test ./... +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke (cached) +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/jira (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane (cached) +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/authoring (cached) +ok github.com/nomadcode/nomadcode-core/internal/config (cached) +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/gitoevents (cached) +ok github.com/nomadcode/nomadcode-core/internal/gitosync (cached) +ok github.com/nomadcode/nomadcode-core/internal/http (cached) +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification (cached) +ok github.com/nomadcode/nomadcode-core/internal/projectsync (cached) +ok github.com/nomadcode/nomadcode-core/internal/protosocket (cached) +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync (cached) +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline (cached) +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.013s +ok github.com/nomadcode/nomadcode-core/internal/storage (cached) +ok github.com/nomadcode/nomadcode-core/internal/workflow (cached) +ok github.com/nomadcode/nomadcode-core/internal/workitem (cached) +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline (cached) + +$ cd services/core && go vet ./... +(No output) + +$ git diff --check +(No output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - correctness: Fail + - completeness: Fail + - test coverage: Fail + - API contract: Pass + - code quality: Pass + - plan deviation: Fail + - verification trust: Pass +- 발견된 문제: + - Required: [services/core/internal/scheduler/roadmap_sync_jobs.go:86](/config/workspace/nomadcode/services/core/internal/scheduler/roadmap_sync_jobs.go:86)의 finalizer payload가 `action`/`reason`만 넘기고 있어, [services/core/internal/workflow/lifecycle.go:213](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:213)에서 완료 시 promote하는 `authoring_run_state`가 계속 기존 `in_progress`로 남습니다. 같은 lifecycle은 [services/core/internal/workflow/lifecycle.go:226](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:226)에서 `summary`만 `status_reason`으로 반영하므로, completed task metadata가 `waiting for pushed develop milestone match`류 stale reason을 유지할 수도 있습니다. `SyncActionProjected`/`SyncActionComplete` finalizer result에 `summary`, `authoring_run_state=succeeded`, 갱신 timestamp를 포함하고, running authoring task가 external-ref finalizer 이후 `completed` + succeeded metadata로 닫히는 regression test를 추가하세요. + - Required: [services/core/internal/scheduler/jobs.go:433](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:433)의 authoring failure path는 `authoring_failure_type`만 기록하고, 계획에서 요구한 failure category metadata를 기록하지 않습니다. [services/core/internal/workflow/model.go:59](/config/workspace/nomadcode/services/core/internal/workflow/model.go:59)의 `MetadataKeyAuthoringFailureCategory` 상수는 현재 참조되지 않아 model bridge error가 `bridge_failed`로 남지 않습니다. authoring failure metadata에 `authoring_failure_category`를 채우고, model bridge error path에서 `authoring_run_state=failed`, `authoring_failure_type`, `authoring_failure_category=bridge_failed`가 함께 저장되는 test를 추가하세요. +- 다음 단계: WARN/FAIL 후속 plan/review 파일을 작성한다. diff --git a/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_1.log b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_1.log new file mode 100644 index 0000000..10d9891 --- /dev/null +++ b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_1.log @@ -0,0 +1,174 @@ + + +# Code Review Reference - REVIEW_API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-15 +task=m-milestone-work-item-creation-sync/12+11_authoring_develop_gate, plan=1, tag=REVIEW_API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md` +- Task ids: + - `authoring-develop-gate`: IOP authoring 응답만으로 task 완료를 단정하지 않고, `develop` Milestone push와 provider identity match가 확인될 때 authoring 성공 또는 sync-ready로 판정된다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-milestone-work-item-creation-sync`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_API-1] Finalizer completion metadata | [x] | +| [REVIEW_API-2] Authoring failure category metadata | [x] | + +## 구현 체크리스트 + +- [x] Finalizer projected/complete payload가 authoring completion metadata와 summary를 포함하고, external-ref completion 후 stale `in_progress` metadata가 남지 않도록 수정한다. +- [x] Authoring failure path가 `authoring_failure_category`를 task failure metadata로 남기도록 수정하고 regression test를 보강한다. +- [x] `go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync`를 통과시킨다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/`를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-milestone-work-item-creation-sync`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-milestone-work-item-creation-sync/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +- `workflow.Service` 의 가짜/모의 객체 테스트(`service_test.go`)를 원활히 처리하기 위해 `lifecycle.go` 에 정의된 `taskStore` 인터페이스에 `GetTaskByExternalRef` 를 추가하고, `CompleteTaskByExternalRef` 가 이 인터페이스 메서드를 사용하도록 수정했습니다. + +## 주요 설계 결정 + +- `workflow.lifecycle` 의 `store` 인터페이스에 `GetTaskByExternalRef` 를 포함시킴으로써 `workflow.Service` 와 `taskStore` 의 결합도를 완화했고, 테스트 코드 내에서 구체 타입인 `*storage.Store` 대신 모의(mock) 테스트 환경을 안전하게 구성할 수 있도록 개선했습니다. + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- finalizer result payload가 `summary`, `authoring_run_state=succeeded`, updated timestamp를 포함하는지 확인한다. +- `CompleteTaskByExternalRef`가 running authoring task의 stale wait metadata를 completed/succeeded metadata로 닫는지 확인한다. +- authoring model bridge failure metadata에 `authoring_failure_category=bridge_failed`가 기록되는지 확인한다. + +## 검증 결과 + +### REVIEW_API-1 중간 검증 +```text +$ go test -count=1 ./internal/scheduler ./internal/workflow +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.033s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.010s +``` + +### REVIEW_API-2 중간 검증 +```text +$ go test -count=1 ./internal/scheduler ./internal/authoring +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.062s +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.007s +``` + +### 최종 검증 +```text +$ go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.013s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.005s +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.006s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.006s +ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.005s + +$ go test ./... +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke (cached) +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/jira (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane (cached) +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/authoring (cached) +ok github.com/nomadcode/nomadcode-core/internal/config (cached) +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/gitoevents (cached) +ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.005s +ok github.com/nomadcode/nomadcode-core/internal/http 0.006s +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification (cached) +ok github.com/nomadcode/nomadcode-core/internal/projectsync (cached) +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.012s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync (cached) +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline (cached) +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.015s +ok github.com/nomadcode/nomadcode-core/internal/storage (cached) +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.004s +ok github.com/nomadcode/nomadcode-core/internal/workitem (cached) +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline (cached) + +$ go vet ./... +(No output) + +$ git diff --check +(No output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS 후 `complete.log` 작성 및 task directory archive 이동을 수행한다. diff --git a/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/complete.log b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/complete.log new file mode 100644 index 0000000..5cf749b --- /dev/null +++ b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/complete.log @@ -0,0 +1,46 @@ +# Complete - m-milestone-work-item-creation-sync/12+11_authoring_develop_gate + +## 완료 일시 + +2026-06-15 + +## 요약 + +Roadmap creation sync authoring/develop gate follow-up review loop completed after 2 reviews; final verdict PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | FAIL | finalizer completion metadata and authoring failure category metadata were incomplete. | +| `plan_cloud_G07_1.log` | `code_review_cloud_G07_1.log` | PASS | Required follow-up issues were implemented and verified. | + +## 구현/정리 내용 + +- Finalizer result payload now includes summary and authoring succeeded metadata for projected/complete creation sync outcomes. +- `CompleteTaskByExternalRef` promotes authoring completion metadata and clears stale wait metadata through the lifecycle completion path. +- Authoring failure metadata now records `authoring_failure_category=bridge_failed` on model bridge failures. +- Review-time formatting cleanup applied with `gofmt` to the touched Go files that needed it. + +## 최종 검증 + +- `go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync` from `services/core` - PASS; focused packages passed. +- `go test ./...` from `services/core` - PASS; all core packages passed. +- `go vet ./...` from `services/core` - PASS; no output. +- `git diff --check` from repository root - PASS; no output. +- `gofmt -l services/core/cmd/server/main.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/internal/scheduler/roadmap_sync_jobs.go services/core/internal/workflow/lifecycle.go services/core/internal/workflow/service.go services/core/internal/workflow/service_test.go services/core/internal/workflow/model.go` - PASS; no output after review cleanup. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md` +- Completed task ids: + - `authoring-develop-gate`: PASS; evidence=`agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_1.log`; verification=`go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync`, `go test ./...`, `go vet ./...`, `git diff --check` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/PLAN-cloud-G07.md b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/plan_cloud_G07_0.log similarity index 100% rename from agent-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/PLAN-cloud-G07.md rename to agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/plan_cloud_G07_0.log diff --git a/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/plan_cloud_G07_1.log b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/plan_cloud_G07_1.log new file mode 100644 index 0000000..54f9493 --- /dev/null +++ b/agent-task/archive/2026/06/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/plan_cloud_G07_1.log @@ -0,0 +1,210 @@ + + +# Plan - REVIEW_API + +## 이 파일을 읽는 구현 에이전트에게 + +구현의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, active 파일을 유지한 채 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 진행할 수 없을 때는 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과에 기록한다. + +## 배경 + +이 후속 작업은 `code_review_cloud_G07_0.log`의 Required 문제만 닫는다. 현재 implementation은 authoring task를 model response-only 상태에서 completed로 만들지 않는 gate는 만들었지만, develop projection 이후 finalizer가 authoring metadata를 성공 상태로 닫지 못한다. 또한 authoring failure path가 계획에서 요구한 failure category metadata를 남기지 않아 runtime 상태 판정 근거가 불완전하다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, 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: + - `authoring-develop-gate`: IOP authoring 응답만으로 task 완료를 단정하지 않고, `develop` Milestone push와 provider identity match가 확인될 때 authoring 성공 또는 sync-ready로 판정된다. +- 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/rules/project/domain/core/rules.md` +- `agent-ops/rules/project/domain/workspace-ops/rules.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/code-review/SKILL.md` +- `agent-ops/skills/common/plan/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-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/plan_cloud_G07_0.log` +- `agent-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/code_review_cloud_G07_0.log` +- `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/11_creation_sync_orchestrator/complete.log` +- `services/core/cmd/server/main.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/internal/scheduler/roadmap_sync_jobs.go` +- `services/core/internal/workflow/service.go` +- `services/core/internal/workflow/lifecycle.go` +- `services/core/internal/workflow/model.go` +- `services/core/internal/workflow/service_test.go` +- `services/core/internal/authoring/request.go` +- `services/core/internal/authoring/result.go` +- `services/core/internal/authoring/result_test.go` +- `services/core/internal/roadmapsyncpipeline/service.go` +- `services/core/internal/roadmapsyncpipeline/service_test.go` +- `services/core/internal/roadmapsync/plane_projection_test.go` +- `services/core/internal/gitosync/bridge.go` +- `services/core/internal/gitosync/bridge_test.go` + +### 테스트 환경 규칙 + +- `test_env=local`. +- `agent-test/local/rules.md` present/read. +- Matched profiles: `agent-test/local/core-smoke.md`, `agent-test/local/workspace-ops-smoke.md`. +- Core 필수 검증: `go test ./...` from `services/core`. +- 보조 검증: `go vet ./...` from `services/core`. +- workspace/task artifact 검증: `git diff --check`. +- Fresh focused regression에는 `-count=1`을 사용한다. 전체 `go test ./...`는 cache output 허용. + +### 테스트 커버리지 공백 + +- `RoadmapCreationSyncWorker` projected/complete finalizer payload가 authoring completion metadata를 담는지 검증하지 않는다. +- `workflow.Service.CompleteTaskByExternalRef`가 running authoring task의 stale `authoring_run_state=in_progress`와 wait reason을 completed/succeeded metadata로 닫는지 검증하지 않는다. +- `TestMarkFailedWithTaskAuthoringRecordsState`는 failure state/timestamp만 보고 `authoring_failure_category`를 검증하지 않는다. + +### 심볼 참조 + +- Renamed/removed symbols: none. +- New/changed symbol call sites: + - `CompleteTaskByExternalRef`: `services/core/internal/scheduler/roadmap_sync_jobs.go`, `services/core/internal/scheduler/river_test.go`, `services/core/internal/workflow/service.go`. + - `MetadataKeyAuthoringFailureCategory`: currently only declared in `services/core/internal/workflow/model.go`; implementation call site missing. + - `MetadataKeyAuthoringRunState`: used in scheduler, lifecycle, tests, and must be present in finalizer result payload. + +### 분할 판단 + +- Split decision policy evaluated. This is a follow-up inside existing dependent subtask `12+11_authoring_develop_gate`; create no new sibling directory. +- Predecessor `11_creation_sync_orchestrator` is satisfied by `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/11_creation_sync_orchestrator/complete.log`. +- Both Required issues affect the same runtime boundary and the same focused verification set, so keeping one follow-up plan avoids artificial coordination overhead. + +### 범위 결정 근거 + +- Do not add new public HTTP/proto-socket APIs. +- Do not change Plane projection ordering, Gito scanner behavior, child ticket creation, or UI. +- Do not alter generic manual/A2A task completion behavior beyond metadata accepted by existing lifecycle completion. + +### 빌드 등급 + +- `cloud-G07`: runtime scheduler/workflow metadata, external-ref finalization, and verification trust cross multiple core packages. + +## 구현 체크리스트 + +- [ ] Finalizer projected/complete payload가 authoring completion metadata와 summary를 포함하고, external-ref completion 후 stale `in_progress` metadata가 남지 않도록 수정한다. +- [ ] Authoring failure path가 `authoring_failure_category`를 task failure metadata로 남기도록 수정하고 regression test를 보강한다. +- [ ] `go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync`를 통과시킨다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REVIEW_API-1] Finalizer completion metadata + +문제: [services/core/internal/scheduler/roadmap_sync_jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/roadmap_sync_jobs.go:86)의 `resultMap`은 `action`과 `reason`만 담는다. [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:213)는 `authoring_run_state`가 result에 있을 때만 task metadata로 promote하고, [services/core/internal/workflow/lifecycle.go](/config/workspace/nomadcode/services/core/internal/workflow/lifecycle.go:226)는 `summary`만 `status_reason`으로 저장한다. 따라서 finalizer completion 후 task status는 `completed`여도 metadata가 `authoring_run_state=in_progress`와 stale wait reason을 유지할 수 있다. + +해결 방법: + +Before: + +```go +// services/core/internal/scheduler/roadmap_sync_jobs.go:86 +resultMap := map[string]any{ + "action": string(res.Action), + "reason": res.Reason, +} +``` + +After: + +```go +// Shape only; keep exact names local to the final code. +resultMap := map[string]any{ + "action": string(res.Action), + "reason": res.Reason, + "summary": res.Reason, + workflow.MetadataKeyAuthoringRunState: "succeeded", + workflow.MetadataKeyAuthoringRunUpdatedAt: time.Now().UTC().Format(time.RFC3339), +} +``` + +수정 파일 및 체크리스트: + +- [ ] `services/core/internal/scheduler/roadmap_sync_jobs.go`: finalizer payload에 `summary`, `authoring_run_state=succeeded`, updated timestamp를 추가한다. +- [ ] `services/core/internal/scheduler/river_test.go`: projected payload assertion을 보강하고 `SyncActionComplete`도 finalizer를 호출하며 동일 metadata를 담는 test를 추가한다. +- [ ] `services/core/internal/workflow/service_test.go`: running authoring task fixture로 `CompleteTaskByExternalRef`가 status completed, `agent_run_state=completed`, `wait_type` 제거, `status_reason=`, `authoring_run_state=succeeded`를 남기는 regression test를 추가한다. + +테스트 작성: `TestRoadmapCreationSyncWorkerFinalizesExternalTaskOnComplete`와 `TestServiceCompleteTaskByExternalRefPromotesAuthoringMetadata`를 추가한다. + +중간 검증: `go test -count=1 ./internal/scheduler ./internal/workflow` from `services/core`가 PASS. + +### [REVIEW_API-2] Authoring failure category metadata + +문제: [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:433)의 authoring failure metadata는 `authoring_run_state`, `authoring_run_updated_at`, `authoring_failure_type`만 저장한다. [services/core/internal/workflow/model.go](/config/workspace/nomadcode/services/core/internal/workflow/model.go:59)의 `MetadataKeyAuthoringFailureCategory`는 선언되어 있지만 call site가 없어, model bridge error가 `bridge_failed` category로 남지 않는다. + +해결 방법: + +Before: + +```go +// services/core/internal/scheduler/jobs.go:435 +failInput.ExtraMetadata = map[string]any{ + workflow.MetadataKeyAuthoringRunState: "failed", + workflow.MetadataKeyAuthoringRunUpdatedAt: time.Now().UTC().Format(time.RFC3339), + workflow.MetadataKeyAuthoringFailureType: string(failureType), +} +``` + +After: + +```go +// Shape only; keep classification consistent with authoring.DecideAuthoringResult. +decision := authoring.DecideAuthoringResult(authoring.DecisionInput{BridgeSuccess: false}) +failInput.ExtraMetadata = map[string]any{ + workflow.MetadataKeyAuthoringRunState: decision.State, + workflow.MetadataKeyAuthoringRunUpdatedAt: time.Now().UTC().Format(time.RFC3339), + workflow.MetadataKeyAuthoringFailureType: string(failureType), + workflow.MetadataKeyAuthoringFailureCategory: decision.FailureCategory, +} +``` + +수정 파일 및 체크리스트: + +- [ ] `services/core/internal/scheduler/jobs.go`: authoring task failure metadata에 `MetadataKeyAuthoringFailureCategory`를 추가한다. +- [ ] `services/core/internal/scheduler/jobs_test.go`: `TestMarkFailedWithTaskAuthoringRecordsState`가 `authoring_failure_category=bridge_failed`를 assert하도록 보강한다. + +테스트 작성: 기존 `TestMarkFailedWithTaskAuthoringRecordsState`에 category assertion을 추가한다. + +중간 검증: `go test -count=1 ./internal/scheduler ./internal/authoring` from `services/core`가 PASS. + +## 의존 관계 및 구현 순서 + +1. `REVIEW_API-1`을 먼저 적용해 successful completion metadata를 닫는다. +2. `REVIEW_API-2`를 적용해 failure metadata를 닫는다. +3. focused tests, full core tests, vet, diff check 순서로 검증한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `services/core/internal/scheduler/roadmap_sync_jobs.go` | REVIEW_API-1 | +| `services/core/internal/scheduler/river_test.go` | REVIEW_API-1 | +| `services/core/internal/workflow/service_test.go` | REVIEW_API-1 | +| `services/core/internal/scheduler/jobs.go` | REVIEW_API-2 | +| `services/core/internal/scheduler/jobs_test.go` | REVIEW_API-2 | + +## 최종 검증 + +- `go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync` from `services/core` +- `go test ./...` from `services/core` +- `go vet ./...` from `services/core` +- `git diff --check` from repository root + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/CODE_REVIEW-cloud-G07.md b/agent-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/CODE_REVIEW-cloud-G07.md deleted file mode 100644 index 5add32c..0000000 --- a/agent-task/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/CODE_REVIEW-cloud-G07.md +++ /dev/null @@ -1,122 +0,0 @@ - - -# Code Review Reference - API - -> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** -> The task is NOT complete until every implementation-owned section below is filled in. -> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. -> Fill implementation-owned sections, then stop with active files in place and report ready for review. -> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. -> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation. -> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only. - -## 개요 - -date=2026-06-15 -task=m-milestone-work-item-creation-sync/12+11_authoring_develop_gate, plan=0, tag=API - -## Roadmap Targets - -- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md` -- Task ids: - - `authoring-develop-gate`: IOP authoring 응답만으로 task 완료를 단정하지 않고, `develop` Milestone push와 provider identity match가 확인될 때 authoring 성공 또는 sync-ready로 판정된다. -- Completion mode: check-on-pass - -## 이 파일을 읽는 리뷰 에이전트에게 - -> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. - -각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. - -1. 판정을 append한다. -2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log`로 아카이브한다. -3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/12+11_authoring_develop_gate/`로 이동한다. -4. PASS이고 task group이 `m-milestone-work-item-creation-sync`이면 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. - ---- - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [API-1] Authoring model response must not complete the task | [ ] | -| [API-2] Creation sync projection finalizes the originating task | [ ] | - -## 구현 체크리스트 - -- [ ] Plane-origin authoring task가 model response-only 상태에서 `completed`로 전이되지 않고 `running` + `authoring_run_state=in_progress` + wait reason을 유지하도록 scheduler/workflow 경계를 수정한다. -- [ ] `RoadmapCreationSyncWorker`가 `SyncActionProjected` 또는 already-complete outcome에서 원래 Plane external ref task를 완료할 수 있는 finalizer seam을 추가한다. -- [ ] model bridge error, push failure, dirty workspace, conflict 같은 failure category는 task failure metadata로 남기고 Plane `Todo` projection으로 넘어가지 않도록 테스트한다. -- [ ] `cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync`를 통과시킨다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. - -## 코드리뷰 전용 체크리스트 - -> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. - -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. -- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. -- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. -- [ ] PASS이면 active task 디렉터리를 archive로 이동한다. -- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 사용자 리뷰 요청 - -_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ - -- 상태: 없음 -- 사유 유형: 없음 -- 결정 필요: 없음 -- 차단 근거: 없음 -- 실행한 검증/명령: 없음 -- 자동 후속 불가 이유: 없음 -- 재개 조건: 없음 - -## 리뷰어를 위한 체크포인트 - -- model response-only authoring task가 `completed`로 전이되지 않는지 확인한다. -- projection terminal outcome만 원래 external-ref task를 완료하는지 확인한다. -- not_ready/conflict/self-mutation outcome에서 provider side effect와 task completion이 섞이지 않는지 확인한다. - -## 검증 결과 - -_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ - -### API-1 중간 검증 -```text -$ cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow -(output) -``` - -### API-2 중간 검증 -```text -$ cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow ./internal/roadmapsyncpipeline -(output) -``` - -### 최종 검증 -```text -$ cd services/core && go test -count=1 ./internal/scheduler ./internal/workflow ./internal/authoring ./internal/roadmapsyncpipeline ./internal/gitosync -(output) -$ cd services/core && go test ./... -(output) -$ cd services/core && go vet ./... -(output) -$ git diff --check -(output) -``` - ---- - -> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go index aa02b97..c7ba16d 100644 --- a/services/core/cmd/server/main.go +++ b/services/core/cmd/server/main.go @@ -104,7 +104,9 @@ func run(logger *slog.Logger) error { // than mis-skipping a real trigger. creationSync := roadmapsyncpipeline.NewService(store, planeClient, "") - taskScheduler, err := scheduler.New(pool, lifecycle, notificationService, agentClient, modelClient, creationSync, time.Duration(cfg.WorkflowTaskTimeoutSec)*time.Second, logger) + workflowService := workflow.NewService(store, nil, logger) + + taskScheduler, err := scheduler.New(pool, lifecycle, notificationService, agentClient, modelClient, creationSync, workflowService, time.Duration(cfg.WorkflowTaskTimeoutSec)*time.Second, logger) if err != nil { return err } @@ -115,6 +117,8 @@ func run(logger *slog.Logger) error { return err } + workflowService.SetEnqueuer(taskScheduler) + // Gito branch event consumer: drives Plane-origin Milestone creation sync // from a develop push. Started only when the endpoint, repo, local develop // checkout, and Todo state id are all configured; otherwise the Core server @@ -132,8 +136,6 @@ func run(logger *slog.Logger) error { logger.Info("gito branch event consumer enabled", "repo_id", cfg.GitoRepoID, "branch", cfg.GitoBranch) } - - workflowService := workflow.NewService(store, taskScheduler, logger) projectBinder := apphttp.NewProjectBinder(store) handler := apphttp.NewHandler( pool, diff --git a/services/core/internal/scheduler/jobs.go b/services/core/internal/scheduler/jobs.go index 66b5421..14a6110 100644 --- a/services/core/internal/scheduler/jobs.go +++ b/services/core/internal/scheduler/jobs.go @@ -100,6 +100,35 @@ func (w *TaskWorker) Work(ctx context.Context, job *river.Job[TaskJobArgs]) erro return err } + // Plane-origin authoring task의 경우, bridge 성공만으로는 task를 완료하지 않고 + // develop match를 대기하는 상태(running)를 유지한다. + _, isAuthoring := authoring.BuildAuthoringGenerateInput(task) + if isAuthoring { + var resMap map[string]any + if err := json.Unmarshal(result, &resMap); err == nil { + if state, ok := resMap[workflow.MetadataKeyAuthoringRunState].(string); ok && state == "in_progress" { + updates := map[string]any{ + workflow.MetadataKeyAuthoringRunState: "in_progress", + workflow.MetadataKeyStatusReason: message, + workflow.MetadataKeyWaitType: "develop_match", + workflow.MetadataKeyAuthoringRunUpdatedAt: time.Now().UTC().Format(time.RFC3339), + } + for k, v := range resMap { + updates[k] = v + } + + if _, mergeErr := w.Lifecycle.MergeTaskMetadata(ctx, taskID, updates); mergeErr != nil && w.Logger != nil { + w.Logger.Warn("authoring running metadata merge failed", "task_id", taskID, "error", mergeErr) + } + + if w.Logger != nil { + w.Logger.Info("task job remains running for develop match", "task_id", taskID) + } + return nil + } + } + } + task, err = w.Lifecycle.CompleteTask(ctx, taskID, result) if err != nil { w.markFailed(taskID, err) @@ -137,12 +166,15 @@ func (w *TaskWorker) runTask(ctx context.Context, task storage.Task) (json.RawMe if err != nil { return nil, "", err } + decision := authoring.DecideAuthoringResult(authoring.DecisionInput{ + BridgeSuccess: true, + }) result := map[string]any{ "message": generated.Text, "mode": "authoring_run", "model": generated.Model, "response_id": generated.ID, - "authoring_run_state": "in_progress", + "authoring_run_state": decision.State, "authoring_run_updated_at": time.Now().UTC().Format(time.RFC3339), "usage": map[string]int{ "input_tokens": generated.Usage.InputTokens, @@ -154,7 +186,7 @@ func (w *TaskWorker) runTask(ctx context.Context, task storage.Task) (json.RawMe if err != nil { return nil, "", err } - return raw, "authoring run completed", nil + return raw, decision.Reason, nil } if w.Agent != nil { @@ -401,10 +433,12 @@ func (w *TaskWorker) markFailedWithTask(taskID string, task storage.Task, err er failInput := workflow.FailureInput{Message: msg, Type: failureType} if _, isAuthoring := authoring.BuildAuthoringGenerateInput(task); isAuthoring { + decision := authoring.DecideAuthoringResult(authoring.DecisionInput{BridgeSuccess: false}) failInput.ExtraMetadata = map[string]any{ - workflow.MetadataKeyAuthoringRunState: "failed", - workflow.MetadataKeyAuthoringRunUpdatedAt: time.Now().UTC().Format(time.RFC3339), - workflow.MetadataKeyAuthoringFailureType: string(failureType), + workflow.MetadataKeyAuthoringRunState: decision.State, + workflow.MetadataKeyAuthoringRunUpdatedAt: time.Now().UTC().Format(time.RFC3339), + workflow.MetadataKeyAuthoringFailureType: string(failureType), + workflow.MetadataKeyAuthoringFailureCategory: decision.FailureCategory, } } diff --git a/services/core/internal/scheduler/jobs_test.go b/services/core/internal/scheduler/jobs_test.go index 9626e4f..6857161 100644 --- a/services/core/internal/scheduler/jobs_test.go +++ b/services/core/internal/scheduler/jobs_test.go @@ -721,6 +721,9 @@ func TestMarkFailedWithTaskAuthoringRecordsState(t *testing.T) { if extra[workflow.MetadataKeyAuthoringRunUpdatedAt] == nil { t.Error("expected authoring_run_updated_at to be set") } + if extra[workflow.MetadataKeyAuthoringFailureCategory] != "bridge_failed" { + t.Errorf("expected authoring_failure_category=bridge_failed, got %#v", extra[workflow.MetadataKeyAuthoringFailureCategory]) + } } func TestMarkFailedWithTaskGenericNoAuthoringState(t *testing.T) { @@ -912,3 +915,51 @@ func TestWorkEmitsFailedEvent(t *testing.T) { t.Error("expected failed occurred_at to be non-zero") } } + +func TestWorkKeepsAuthoringTaskRunningUntilDevelopMatch(t *testing.T) { + fakeModel := fakeModelClient{ + generate: func(_ context.Context, _ model.GenerateInput) (model.GenerateResult, error) { + return model.GenerateResult{Text: "milestone authored", Model: "m"}, nil + }, + } + + fakeLifecycle := &fakeTaskLifecycle{ + task: storage.Task{ + ID: "task-auth-wait", + Source: "plane", + Status: "pending", + Metadata: checkoutTaskMeta("/home/user/workspace/nomadcode/slots/000", "develop"), + }, + } + + worker := &TaskWorker{ + Lifecycle: fakeLifecycle, + Model: fakeModel, + } + + job := &river.Job[TaskJobArgs]{Args: TaskJobArgs{TaskID: "task-auth-wait"}} + if err := worker.Work(context.Background(), job); err != nil { + t.Fatalf("Work returned error: %v", err) + } + + if len(fakeLifecycle.completed) != 0 { + t.Fatalf("expected CompletedTask to NOT be called, but was called: %v", fakeLifecycle.completed) + } + + foundWaitType := false + foundRunState := false + for _, m := range fakeLifecycle.mergedMetadata { + if m[workflow.MetadataKeyWaitType] == "develop_match" { + foundWaitType = true + } + if m[workflow.MetadataKeyAuthoringRunState] == "in_progress" { + foundRunState = true + } + } + if !foundWaitType { + t.Errorf("expected MergeTaskMetadata to be called with wait_type=develop_match, got %v", fakeLifecycle.mergedMetadata) + } + if !foundRunState { + t.Errorf("expected MergeTaskMetadata to be called with authoring_run_state=in_progress, got %v", fakeLifecycle.mergedMetadata) + } +} diff --git a/services/core/internal/scheduler/river.go b/services/core/internal/scheduler/river.go index a8f5024..09230eb 100644 --- a/services/core/internal/scheduler/river.go +++ b/services/core/internal/scheduler/river.go @@ -27,7 +27,7 @@ type Client struct { // Milestone creation sync orchestrator: when non-nil its worker is registered so // EnqueueRoadmapCreationSync has a runtime worker; when nil only the task worker // is registered, preserving the default task scheduler behavior. -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) { +func New(pool *pgxpool.Pool, lifecycle TaskLifecycle, notifications *notification.Service, agentClient agent.Client, modelClient model.Client, creationSync CreationSyncRunner, finalizer TaskFinalizer, runTimeout time.Duration, logger *slog.Logger) (*Client, error) { workers := river.NewWorkers() river.AddWorker(workers, &TaskWorker{ Lifecycle: lifecycle, @@ -39,8 +39,9 @@ func New(pool *pgxpool.Pool, lifecycle TaskLifecycle, notifications *notificatio }) if creationSync != nil { river.AddWorker(workers, &RoadmapCreationSyncWorker{ - Sync: creationSync, - Logger: logger, + Sync: creationSync, + TaskFinalizer: finalizer, + Logger: logger, }) } diff --git a/services/core/internal/scheduler/river_test.go b/services/core/internal/scheduler/river_test.go index e58b1e8..6242aba 100644 --- a/services/core/internal/scheduler/river_test.go +++ b/services/core/internal/scheduler/river_test.go @@ -2,6 +2,7 @@ package scheduler import ( "context" + "encoding/json" "errors" "testing" @@ -10,6 +11,7 @@ import ( "github.com/nomadcode/nomadcode-core/internal/roadmapsync" "github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline" + "github.com/nomadcode/nomadcode-core/internal/workflow" "github.com/nomadcode/nomadcode-core/internal/workitem" ) @@ -81,6 +83,125 @@ func TestRoadmapCreationSyncWorkerReturnsServiceError(t *testing.T) { } } +type fakeTaskFinalizer struct { + called bool + provider string + id string + result []byte +} + +func (f *fakeTaskFinalizer) CompleteTaskByExternalRef(ctx context.Context, provider, id string, result json.RawMessage) error { + f.called = true + f.provider = provider + f.id = id + f.result = result + return nil +} + +func TestRoadmapCreationSyncWorkerFinalizesExternalTaskOnProjected(t *testing.T) { + runner := &fakeCreationSyncRunner{ + result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionProjected, Reason: "projected successfully"}, + } + finalizer := &fakeTaskFinalizer{} + worker := &RoadmapCreationSyncWorker{ + Sync: runner, + TaskFinalizer: finalizer, + } + + args := sampleCreationSyncArgs() + err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args}) + if err != nil { + t.Fatalf("Work returned error: %v", err) + } + if !runner.called { + t.Fatal("expected orchestrator to be called") + } + if !finalizer.called { + t.Fatal("expected finalizer to be called on projected outcome") + } + if finalizer.provider != "plane" || finalizer.id != "NOMAD-1" { + t.Errorf("unexpected finalizer arguments: provider=%q, id=%q", finalizer.provider, finalizer.id) + } + var resMap map[string]any + if err := json.Unmarshal(finalizer.result, &resMap); err != nil { + t.Fatalf("fail to unmarshal finalizer result: %v", err) + } + if resMap["action"] != "projected" || resMap["reason"] != "projected successfully" { + t.Errorf("unexpected finalizer result payload: %v", resMap) + } + if resMap["summary"] != "projected successfully" { + t.Errorf("expected summary payload, got %v", resMap["summary"]) + } + if resMap[workflow.MetadataKeyAuthoringRunState] != "succeeded" { + t.Errorf("expected authoring_run_state succeeded, got %v", resMap[workflow.MetadataKeyAuthoringRunState]) + } + if resMap[workflow.MetadataKeyAuthoringRunUpdatedAt] == nil || resMap[workflow.MetadataKeyAuthoringRunUpdatedAt] == "" { + t.Error("expected authoring_run_updated_at to be set") + } +} + +func TestRoadmapCreationSyncWorkerFinalizesExternalTaskOnComplete(t *testing.T) { + runner := &fakeCreationSyncRunner{ + result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionComplete, Reason: "already complete"}, + } + finalizer := &fakeTaskFinalizer{} + worker := &RoadmapCreationSyncWorker{ + Sync: runner, + TaskFinalizer: finalizer, + } + + args := sampleCreationSyncArgs() + err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args}) + if err != nil { + t.Fatalf("Work returned error: %v", err) + } + if !runner.called { + t.Fatal("expected orchestrator to be called") + } + if !finalizer.called { + t.Fatal("expected finalizer to be called on complete outcome") + } + if finalizer.provider != "plane" || finalizer.id != "NOMAD-1" { + t.Errorf("unexpected finalizer arguments: provider=%q, id=%q", finalizer.provider, finalizer.id) + } + var resMap map[string]any + if err := json.Unmarshal(finalizer.result, &resMap); err != nil { + t.Fatalf("fail to unmarshal finalizer result: %v", err) + } + if resMap["action"] != "complete" || resMap["reason"] != "already complete" { + t.Errorf("unexpected finalizer result payload: %v", resMap) + } + if resMap["summary"] != "already complete" { + t.Errorf("expected summary payload, got %v", resMap["summary"]) + } + if resMap[workflow.MetadataKeyAuthoringRunState] != "succeeded" { + t.Errorf("expected authoring_run_state succeeded, got %v", resMap[workflow.MetadataKeyAuthoringRunState]) + } +} + +func TestRoadmapCreationSyncWorkerDoesNotFinalizeNotReady(t *testing.T) { + runner := &fakeCreationSyncRunner{ + result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionNotReady, Reason: "not ready yet"}, + } + finalizer := &fakeTaskFinalizer{} + worker := &RoadmapCreationSyncWorker{ + Sync: runner, + TaskFinalizer: finalizer, + } + + args := sampleCreationSyncArgs() + err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args}) + if err != nil { + t.Fatalf("Work returned error: %v", err) + } + if !runner.called { + t.Fatal("expected orchestrator to be called") + } + if finalizer.called { + t.Fatal("expected finalizer to NOT be called on not_ready outcome") + } +} + // lazyPool builds a pgxpool without dialing. pgxpool.New is lazy and only // connects on first acquire, so it is safe for constructing the scheduler in a // unit test that never starts the client. @@ -96,7 +217,7 @@ func lazyPool(t *testing.T) *pgxpool.Pool { func TestSchedulerRegistersRoadmapSyncWorkerWhenConfigured(t *testing.T) { runner := &fakeCreationSyncRunner{} - client, err := New(lazyPool(t), nil, nil, nil, nil, runner, 0, nil) + client, err := New(lazyPool(t), nil, nil, nil, nil, runner, nil, 0, nil) if err != nil { t.Fatalf("New with creation sync runner: %v", err) } @@ -110,7 +231,7 @@ func TestSchedulerRegistersRoadmapSyncWorkerWhenConfigured(t *testing.T) { } func TestSchedulerWithoutRoadmapSyncKeepsTaskWorkerOnlyBehavior(t *testing.T) { - client, err := New(lazyPool(t), nil, nil, nil, nil, nil, 0, nil) + client, err := New(lazyPool(t), nil, nil, nil, nil, nil, nil, 0, nil) if err != nil { t.Fatalf("New without creation sync runner: %v", err) } diff --git a/services/core/internal/scheduler/roadmap_sync_jobs.go b/services/core/internal/scheduler/roadmap_sync_jobs.go index b98590e..985b672 100644 --- a/services/core/internal/scheduler/roadmap_sync_jobs.go +++ b/services/core/internal/scheduler/roadmap_sync_jobs.go @@ -2,12 +2,15 @@ package scheduler import ( "context" + "encoding/json" "log/slog" + "time" "github.com/riverqueue/river" "github.com/nomadcode/nomadcode-core/internal/roadmapsync" "github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline" + "github.com/nomadcode/nomadcode-core/internal/workflow" "github.com/nomadcode/nomadcode-core/internal/workitem" ) @@ -45,6 +48,10 @@ type CreationSyncRunner interface { SyncCreation(ctx context.Context, in roadmapsyncpipeline.SyncCreationInput) (roadmapsyncpipeline.SyncCreationResult, error) } +type TaskFinalizer interface { + CompleteTaskByExternalRef(ctx context.Context, provider string, id string, result json.RawMessage) error +} + // RoadmapCreationSyncWorker runs a creation sync job by delegating to the // orchestrator. Not-ready/conflict/self-mutation/complete are normal terminal // outcomes the orchestrator already guards with no provider side effect, so the @@ -54,8 +61,9 @@ type CreationSyncRunner interface { type RoadmapCreationSyncWorker struct { river.WorkerDefaults[RoadmapCreationSyncJobArgs] - Sync CreationSyncRunner - Logger *slog.Logger + Sync CreationSyncRunner + TaskFinalizer TaskFinalizer + Logger *slog.Logger } func (w *RoadmapCreationSyncWorker) Work(ctx context.Context, job *river.Job[RoadmapCreationSyncJobArgs]) error { @@ -76,6 +84,27 @@ func (w *RoadmapCreationSyncWorker) Work(ctx context.Context, job *river.Job[Roa if err != nil { return err } + + if (res.Action == roadmapsyncpipeline.SyncActionProjected || res.Action == roadmapsyncpipeline.SyncActionComplete) && w.TaskFinalizer != nil { + resultMap := map[string]any{ + "action": string(res.Action), + "reason": res.Reason, + "summary": res.Reason, + workflow.MetadataKeyAuthoringRunState: "succeeded", + workflow.MetadataKeyAuthoringRunUpdatedAt: time.Now().UTC().Format(time.RFC3339), + } + if res.NextStep != "" { + resultMap["next_step"] = string(res.NextStep) + } + resultBytes, err := json.Marshal(resultMap) + if err != nil { + return err + } + if err := w.TaskFinalizer.CompleteTaskByExternalRef(ctx, string(args.Ref.Provider), args.Ref.ID, resultBytes); err != nil { + return err + } + } + if w.Logger != nil { w.Logger.Info("roadmap creation sync completed", "work_item_id", args.Expected.WorkItemID, diff --git a/services/core/internal/workflow/lifecycle.go b/services/core/internal/workflow/lifecycle.go index e1b540a..e2a5739 100644 --- a/services/core/internal/workflow/lifecycle.go +++ b/services/core/internal/workflow/lifecycle.go @@ -13,6 +13,7 @@ import ( type taskStore interface { CreateTask(context.Context, storage.CreateTaskInput) (storage.Task, error) GetTask(context.Context, string) (storage.Task, error) + GetTaskByExternalRef(context.Context, string, string) (storage.Task, error) ListTasks(context.Context, int32) ([]storage.Task, error) UpdateMetadata(context.Context, string, json.RawMessage) (storage.Task, error) UpdateStatus(context.Context, string, string) (storage.Task, error) @@ -119,7 +120,6 @@ func currentTaskAttempt(metadata json.RawMessage) int { return 0 } - func mergeTaskMetadata(current json.RawMessage, updates map[string]any) (json.RawMessage, error) { var currentMap map[string]any if len(current) > 0 && string(current) != "null" { @@ -319,7 +319,6 @@ func (l *Lifecycle) MergeTaskMetadata(ctx context.Context, id string, updates ma return l.store.UpdateMetadata(ctx, id, newMeta) } - func (l *Lifecycle) CancelTask(ctx context.Context, id string, message string) (storage.Task, error) { task, err := l.store.GetTask(ctx, id) if err != nil { diff --git a/services/core/internal/workflow/model.go b/services/core/internal/workflow/model.go index 8a57837..4a078be 100644 --- a/services/core/internal/workflow/model.go +++ b/services/core/internal/workflow/model.go @@ -54,10 +54,8 @@ const ( // authoring_run_state values: "in_progress", "succeeded", "failed". // Stale detection uses authoring_run_updated_at compared against a // configured staleness threshold — no separate timeout state is stored. - MetadataKeyAuthoringRunState = "authoring_run_state" - MetadataKeyAuthoringRunUpdatedAt = "authoring_run_updated_at" - MetadataKeyAuthoringFailureType = "authoring_failure_type" + MetadataKeyAuthoringRunState = "authoring_run_state" + MetadataKeyAuthoringRunUpdatedAt = "authoring_run_updated_at" + MetadataKeyAuthoringFailureType = "authoring_failure_type" MetadataKeyAuthoringFailureCategory = "authoring_failure_category" ) - - diff --git a/services/core/internal/workflow/service.go b/services/core/internal/workflow/service.go index b0e38d9..b714dc6 100644 --- a/services/core/internal/workflow/service.go +++ b/services/core/internal/workflow/service.go @@ -231,3 +231,21 @@ func (s *Service) EnqueueTask(ctx context.Context, id string) (storage.Task, err func canEnqueue(status string) bool { return canEnqueueStatus(TaskStatus(status)) } + +func (s *Service) SetEnqueuer(enqueuer TaskEnqueuer) { + s.enqueuer = enqueuer +} + +func (s *Service) CompleteTaskByExternalRef(ctx context.Context, provider, id string, result json.RawMessage) error { + _, err := s.WithExternalRefLock(ctx, provider, id, func(ctx context.Context) (storage.Task, error) { + task, err := s.lifecycle.store.GetTaskByExternalRef(ctx, provider, id) + if err != nil { + return storage.Task{}, err + } + if task.Status == string(StatusCompleted) { + return task, nil + } + return s.lifecycle.CompleteTask(ctx, task.ID, result) + }) + return err +} diff --git a/services/core/internal/workflow/service_test.go b/services/core/internal/workflow/service_test.go index 9f2f735..f8e4082 100644 --- a/services/core/internal/workflow/service_test.go +++ b/services/core/internal/workflow/service_test.go @@ -761,3 +761,55 @@ func TestServiceGetTaskByExternalRef(t *testing.T) { t.Errorf("expected ErrInvalidTaskInput for empty external ID, got %v", err) } } + +func TestServiceCompleteTaskByExternalRefPromotesAuthoringMetadata(t *testing.T) { + ctx := context.Background() + store := newFakeTaskStore() + + provider := "plane" + extID := "NOMAD-42" + + store.tasks["task-auth-complete"] = storage.Task{ + ID: "task-auth-complete", + Status: string(StatusRunning), + ExternalProvider: &provider, + ExternalID: &extID, + Metadata: json.RawMessage(`{"attempt":1,"agent_run_state":"running","wait_type":"develop_match","authoring_run_state":"in_progress"}`), + } + + service := NewService(nil, nil, nil) + service.lifecycle = &Lifecycle{store: store} + + result := json.RawMessage(`{"summary":"completed via scan","authoring_run_state":"succeeded"}`) + err := service.CompleteTaskByExternalRef(ctx, "plane", "NOMAD-42", result) + if err != nil { + t.Fatalf("CompleteTaskByExternalRef returned error: %v", err) + } + + task, err := store.GetTask(ctx, "task-auth-complete") + if err != nil { + t.Fatalf("GetTask returned error: %v", err) + } + + if task.Status != string(StatusCompleted) { + t.Errorf("expected status completed, got %s", task.Status) + } + + var meta map[string]any + if err := json.Unmarshal(task.Metadata, &meta); err != nil { + t.Fatalf("failed to unmarshal metadata: %v", err) + } + + if meta[MetadataKeyAgentRunState] != "completed" { + t.Errorf("expected agent_run_state completed, got %v", meta[MetadataKeyAgentRunState]) + } + if meta[MetadataKeyAuthoringRunState] != "succeeded" { + t.Errorf("expected authoring_run_state succeeded, got %v", meta[MetadataKeyAuthoringRunState]) + } + if val, ok := meta[MetadataKeyWaitType]; ok && val != nil { + t.Errorf("expected wait_type to be cleared, got %v", val) + } + if meta[MetadataKeyStatusReason] != "completed via scan" { + t.Errorf("expected status_reason 'completed via scan', got %v", meta[MetadataKeyStatusReason]) + } +}