feat(project-sync): 프로젝트 동기화 저장 경계를 추가한다

마일스톤 작업 생성 동기화를 진행하기 위해 Plane project별 active sync 설정을 Core DB에 저장하고 조회할 수 있는 persistence 경계와 검증 산출물을 함께 정리한다.
This commit is contained in:
toki 2026-06-06 18:56:37 +09:00
parent d9867fe5c9
commit 488a2e6c6a
30 changed files with 3223 additions and 124 deletions

View file

@ -4,7 +4,7 @@
NomadCode는 Flutter 기반 앱, core 서비스, 공유 계약, agent-operation 규칙을 하나의 원레포로 묶어 AI-assisted development workflow를 조율하는 프로젝트다. NomadCode는 Flutter 기반 앱, core 서비스, 공유 계약, agent-operation 규칙을 하나의 원레포로 묶어 AI-assisted development workflow를 조율하는 프로젝트다.
현재 로드맵은 `ROADMAP.md -> phase/<phase-slug>/PHASE.md -> phase/<phase-slug>/milestones/<milestone-slug>.md` scaffold를 기준으로 관리한다. React/Vite 웹 콘솔 제거, 서버/Plane/provider 기반 작업, Flutter-first 클라이언트 정리, Mattermost push plugin extraction, client integration 표준화는 완료되었다. 이후 core workflow 안정화, 외부 통합, Flutter-first 프로젝트 제어 UX를 먼저 정리하고, MCP 기반 agent-ops 제어 표면은 로드맵의 마지막으로 미룬다. 현재 로드맵은 `ROADMAP.md -> phase/<phase-slug>/PHASE.md -> phase/<phase-slug>/milestones/<milestone-slug>.md` scaffold를 기준으로 관리한다. React/Vite 웹 콘솔 제거, 서버/Plane/provider 기반 작업, Flutter-first 클라이언트 정리, Mattermost push plugin extraction, client integration 표준화는 완료되었다. 현재는 Project Workspace Management UX와 Agent-Ops MCP Control Plane이 진행중이며, Agent-Ops MCP Control Plane은 Milestone 생성 동기화를 Plane `Todo` 검토 상태까지 닫는 첫 slice를 다룬다.
IOP 외부 실행 호출은 OpenAI-compatible Responses API 방식을 기본 계약으로 채택하고, NomadCode/IOP 고유의 task, workspace, session, approval, artifact, notification 문맥은 별도 `iop` wrapper field가 아니라 `metadata` 확장으로 전달한다. A2A는 agent-to-agent delegation이 명확히 필요할 때 재검토하며, IOP native protocol은 NomadCode의 기본 외부 실행 호출 표면으로 쓰지 않는다. IOP 외부 실행 호출은 OpenAI-compatible Responses API 방식을 기본 계약으로 채택하고, NomadCode/IOP 고유의 task, workspace, session, approval, artifact, notification 문맥은 별도 `iop` wrapper field가 아니라 `metadata` 확장으로 전달한다. A2A는 agent-to-agent delegation이 명확히 필요할 때 재검토하며, IOP native protocol은 NomadCode의 기본 외부 실행 호출 표면으로 쓰지 않는다.
@ -38,9 +38,9 @@ IOP 외부 실행 호출은 OpenAI-compatible Responses API 방식을 기본 계
- [진행중] Project Workspace Management UX - [진행중] Project Workspace Management UX
- 경로: `agent-roadmap/phase/project-workspace-management-ux/PHASE.md` - 경로: `agent-roadmap/phase/project-workspace-management-ux/PHASE.md`
- 요약: client integration 표준화, core workflow, 외부 통합 기준 이후 프로젝트 단위 앱 UX의 desktop/mobile 역할과 IOP/task 결과 표시 기준을 정리한다. - 요약: client integration 표준화, core workflow, 외부 통합 기준 이후 프로젝트 단위 앱 UX의 desktop/mobile 역할과 IOP/task 결과 표시 기준을 정리한다.
- [계획] Agent-Ops MCP Control Plane - [진행중] Agent-Ops MCP Control Plane
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md` - 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- 요약: 로드맵 기반 agent-ops 운영 자동화와 외부 agent용 MCP 제어 표면은 현재 작업 후보에서 제외하고 로드맵 마지막 Phase로 둔다. - 요약: 로드맵 기반 agent-ops 운영 자동화, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent용 MCP 제어 표면을 다룬다. 현재 작업은 Milestone 생성 동기화를 Plane Todo 검토 상태까지 닫는 첫 slice다.
## 로딩 정책 ## 로딩 정책

View file

@ -2,11 +2,11 @@
## 상태 ## 상태
[계획] [진행중]
## 목표 ## 목표
Workflow Core, External Integration, Project Workspace Management UX가 정리된 뒤, 로드맵 기반 agent-ops 운영 루프와 외부 agent 제어 표면을 Core action과 MCP tool 계층으로 분리한다. 현재 작업 후보가 아니라 장기 운영 자동화와 외부 agent 제어를 다루는 마지막 Phase로 둔다. 로드맵 기반 agent-ops 운영 루프, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent 제어 표면을 Core action과 MCP tool 계층으로 분리한다. 현재 작업은 Milestone 생성 동기화를 Plane `Todo` 검토 상태까지 닫는 첫 slice다.
## Milestone 흐름 ## Milestone 흐름
@ -14,13 +14,23 @@ Workflow Core, External Integration, Project Workspace Management UX가 정리
완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. 완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다. 스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다.
- [진행중] Milestone Work Item Creation Sync
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- 요약: 먼저 진행할 첫 구현 slice다. Plane-origin 방식으로 Milestone을 생성하고 Plane Todo 티켓까지 동기화한 뒤 멈춘다. Workspace agent 실행은 IOP CLI 1차 통로를 선행으로 두며, Agent chat/IDE-origin 생성 projection은 이번 범위에서 제외한다.
- [계획] Roadmap Driven Agent-Ops Automation - [계획] Roadmap Driven Agent-Ops Automation
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/roadmap-driven-agent-ops-automation.md` - 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/roadmap-driven-agent-ops-automation.md`
- 요약: 외부 agent가 MCP를 통해 roadmap/action core를 제어하는 운영 자동화와 Plane 기반 검토/승인 흐름을 후속 Phase에서 정리한다. - 요약: roadmap/action core와 Plane/Jira 기반 Milestone item 동기화의 상위 방향과 계약을 정리하는 설계 마일스톤이며, 실제 구현은 slice 마일스톤으로 진행한다.
- [계획] Milestone Execution Lifecycle Sync
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-execution-lifecycle-sync.md`
- 요약: Todo 이후 In Progress 실행, 하위 티켓, plan/code-review, 완료/폐기 흐름은 사용자가 명시적으로 해제할 때까지 구현 잠금 상태로 둔다.
## Phase 경계 ## Phase 경계
- 이 Phase는 현재 Workflow Core 구현의 선행 조건이 아니다. - 이 Phase는 현재 Workflow Core 구현의 선행 조건이 아니다.
- Core task lifecycle, proto-socket client-core 통신, provider adapter 기본 통합, Flutter workbench UX가 먼저 닫힌 뒤 진행한다. - Core task lifecycle, proto-socket client-core 통신, provider adapter 기본 통합, Flutter workbench UX가 먼저 닫힌 뒤 진행한다.
- Plane/Jira work item과 agent-roadmap Milestone item의 양방향 동기화는 별도 Core sync domain이 소유하며, provider adapter 세부 구현과 분리한다.
- Sync는 Plane/Jira provider project 단위 project sync 설정을 통해 provider project target, git remote, 실제 작업 workspace를 확정한 뒤 진행한다.
- MCP 서버, 외부 agent tool policy, roadmap/action side effect 제어는 이 Phase에서 다룬다. - MCP 서버, 외부 agent tool policy, roadmap/action side effect 제어는 이 Phase에서 다룬다.
- IOP 내부 모델 라우팅, RAG, context compression, output validation은 NomadCode 범위에서 제외한다. - IOP 내부 모델 라우팅, RAG, context compression, output validation은 NomadCode 범위에서 제외한다.

View file

@ -0,0 +1,68 @@
# Milestone: Milestone Execution Lifecycle Sync
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
## 목표
Plane `Todo` 검토 이후 사용자가 `In Progress`로 옮긴 상위 티켓을 실행 승인으로 해석하고, Milestone Task를 Plane 하위 티켓과 `agent-task/m-<milestone-id>` plan/code-review lifecycle에 연결한다. 이 마일스톤은 사용자가 명시적으로 해제할 때까지 실행하지 않는다.
## 상태
[계획]
## 구현 잠금
- 상태: 잠금
- 결정 필요:
- [ ] 사용자가 `Milestone Work Item Creation Sync` 확인 후 이 마일스톤의 잠금 해제를 명시한다.
## 범위
- Plane 상위 티켓 `Todo -> In Progress`를 실행 승인으로 해석하는 흐름
- 1하위 티켓 = 1 Milestone Task identity mapping
- Milestone Task를 Plane 하위 티켓으로 생성하거나 기존 하위 티켓과 매칭하는 흐름
- `agent-task/m-<milestone-id>` plan/code-review lifecycle과 Plane 하위 티켓 상태 동기화
- 막힌 작업의 `User Review` 전환과 정상 PASS의 `Done` 전환
- 상위 Milestone 티켓의 `User Review`, `Done`, `Cancelled` 후속 처리
## 기능
### Epic: [execution-sync] Milestone execution lifecycle sync
Todo 이후 실행 lifecycle을 Plane 하위 티켓, Milestone Task, agent-task 결과와 연결한다.
- [ ] [exec-gate] 사용자가 Plane 상위 티켓을 `In Progress`로 옮길 때만 실행 lifecycle을 시작한다. 검증: `Todo`는 검토 상태이고 자동 실행 gate가 아님이 유지된다.
- [ ] [child-map] Milestone Task와 Plane 하위 티켓의 identity mapping을 정한다. 검증: 1하위 티켓이 1 Milestone Task에만 연결된다.
- [ ] [child-create] Milestone Task를 Plane 하위 티켓으로 생성하거나 기존 하위 티켓과 매칭한다. 검증: 재시도 시 중복 하위 티켓을 만들지 않는다.
- [ ] [agent-task-loop] Plane 하위 티켓과 `agent-task/m-<milestone-id>` plan/code-review lifecycle을 연결한다. 검증: PASS/WARN/FAIL/User Review 결과가 각 하위 티켓 상태에 일관되게 반영된다.
- [ ] [parent-review] 모든 하위 작업 완료 후 상위 Milestone 티켓을 `User Review`로 이동하는 기준을 정한다. 검증: 사용자의 최종 확인 전에는 archive 이동을 하지 않는다.
- [ ] [parent-close] 사용자가 상위 티켓을 `Done` 또는 `Cancelled`로 옮길 때 agent-roadmap Milestone 상태와 완료 리뷰를 어떻게 갱신할지 정한다. 검증: 완료와 폐기 모두 사용자 승인 경계를 지킨다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 없음
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- Plane-origin 또는 Agent-origin Milestone 생성 동기화
- sync domain의 provider-neutral 계약 초안
- MCP tool policy와 외부 agent tool permission 상세
- 사용자 승인 없는 자동 archive 이동
## 작업 컨텍스트
- 관련 경로: `services/core/internal/workitem/`, `services/core/internal/scheduler/`, `services/core/internal/workflow/`, `agent-task/`, `packages/contracts/`, `agent-roadmap/`
- 선행 작업: `Milestone Work Item Creation Sync`
- 잠금 해제 조건: 사용자가 명시적으로 이 마일스톤의 잠금 해제를 요청한다.
- 현재 지점: 사용자 확인과 명시적 잠금 해제 전까지 실행하지 않는 후속 마일스톤이다.
- 확인 필요:
- [ ] 잠금 해제 후 `Todo -> In Progress` 이후 범위를 다시 확인한다.

View file

@ -0,0 +1,127 @@
# Milestone: Milestone Work Item Creation Sync
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
## 목표
Plane-origin 경로로 Milestone을 생성하고, `develop` branch의 agent-roadmap Milestone을 source of truth로 삼아 Plane 상위 티켓과 같은 identity로 연결해 `Todo` 검토 상태까지 동기화한다. 이 마일스톤의 완료 지점은 사용자가 Plane Todo에서 `develop`에 반영된 Milestone 내용을 검토할 수 있는 상태이며, 여기서 멈춘다.
## 상태
[진행중]
## 구현 잠금
- 상태: 해제
- 결정 필요: 없음
- 부분 잠금:
- `plane-agent-authoring`: IOP CLI 1차 통로로 workspace agent authoring run을 실행할 수 있을 때까지 실행 잠금
## 범위
- Plane `Backlog + AGENT assignee` 상위 티켓을 Milestone 생성 trigger로 처리하는 흐름
- Plane project 단위 sync 설정 모델과 DB 저장 흐름
- Plane project 대상, git remote, 실제 작업 workspace를 project sync 기본 설정으로 관리하는 흐름
- NomadCode host 기준 workspace base path, repo별 project workspace root, 3자리 slot index 기반 병렬 workspace layout 정의
- Workspace slot의 사용중 상태를 확인하고 비어 있는 slot만 병렬 작업에 할당하는 흐름
- 안정성을 우선해 slot별 독립 git checkout을 기본으로 두는 흐름
- Plane 티켓 원본 본문을 `사용자 요청:` 댓글로 보존하는 흐름
- `develop` branch의 agent-roadmap Milestone을 source of truth로 삼는 흐름
- Milestone sync 확정 시점을 `develop` branch commit/push 완료로 보는 흐름
- `develop` push 완료를 sync layer가 감지한 뒤 Plane 티켓을 갱신하고 `Todo`로 이동하는 흐름
- `develop`에 반영된 Milestone을 기준으로 Plane 본문을 Milestone 내용으로 치환하는 흐름
- Plane 제목을 `[milestone-id] 제목` 형식으로 바꾸고 `Todo`로 이동하는 흐름. 여기서 `milestone-id``develop`의 Milestone 파일 slug를 기본값으로 쓴다.
- Plane 티켓 제목/본문을 입력으로 workspace agent가 Milestone 파일을 직접 작성하고 `develop`에 push하는 authoring 경계
- Workspace agent authoring run을 IOP CLI로 실행하는 1차 통로와 실행 가능 여부 잠금 기준
- Plane 티켓과 pushed Milestone을 연결하는 provider/work item identity 매칭 기준
- Authoring run의 최소 실행 상태와 stale/failed 판단 기준
- Pushed commit에서 실제 Milestone 파일 변경과 Plane ticket identity를 검증한 뒤 Plane을 갱신하는 기준
- Milestone path와 Plane work item id의 identity map, Plane work item id 기반 idempotency, actor guard, 부분 실패 재시도 기준
## 기능
### Epic: [project-config] Project sync configuration
Plane project 단위로 Milestone sync에 필요한 기본 설정을 저장하고 이후 UI/UX가 소비할 project settings 경계를 만든다.
- [ ] [project-model] Plane project 단위 sync 설정 모델을 정의한다. 검증: 설정 모델이 provider project target, git remote URL, source-of-truth branch, workspace id/path를 필수 필드로 갖는다.
- [ ] [project-store] Project sync 설정을 Core DB에 저장하는 persistence 경계를 정한다. 검증: Plane project별로 하나의 active sync 설정을 찾을 수 있고, Milestone 생성 동기화가 이 설정 없이는 진행되지 않는 기준이 문서화되어 있다.
- [ ] [project-bind] Plane-origin 생성 동기화가 project sync 설정을 통해 Plane project, git repository, 작업 workspace를 해석하도록 한다. 검증: 같은 Plane project 안의 티켓은 같은 git/workspace 설정을 사용하며, 다른 Plane project와 섞이지 않는다.
- [ ] [workspace-layout] NomadCode host 기준 workspace 경로 규칙을 정의한다. 검증: `workspace_base_path=/home/user/workspace`, `repo_dir_name=nomadcode`이면 project workspace root는 `/home/user/workspace/nomadcode`이고 실제 작업 slot path는 `/home/user/workspace/nomadcode/000`처럼 계산된다.
- [ ] [workspace-slot] 병렬 운용을 위한 workspace slot index 모델을 정의한다. 검증: 기본 작업공간은 `000`이고, 병렬 요청이 들어오면 `001`, `002`처럼 3자리 zero-padded index를 증가시키며 각 slot이 독립 작업공간으로 예약된다.
- [ ] [workspace-slot-state] Workspace slot의 사용중 상태 모델을 정의한다. 검증: 각 slot은 `available`, `in_use`, `dirty`, `error` 같은 상태 후보를 갖고, 새 작업은 `available` slot만 선택하며, 동시에 들어온 요청이 같은 slot을 잡지 않도록 DB에서 원자적으로 `in_use`로 전환하는 기준이 있다.
- [ ] [workspace-checkout] Git repository checkout 정책을 project sync 설정과 연결한다. 검증: git remote는 project sync 설정에서 읽고, 안정성을 우선해 각 slot은 같은 remote의 독립 checkout을 기본으로 확보하며, 일반 단일 운용에서는 `000`만 사용한다.
- [ ] [settings-ui] Project sync 설정을 이후 프로젝트 설정 UI/UX에서 관리해야 하는 후보로 남긴다. 검증: 이번 마일스톤에서는 UI 구현을 하지 않고, 후속 Project settings 화면이 provider project target, git remote, workspace를 표시/수정해야 한다는 요구가 남아 있다.
### Epic: [agent-bridge] Workspace agent IOP CLI bridge
Plane-origin sync가 workspace agent를 실행할 수 있도록 IOP CLI 기반 1차 실행 통로를 선행으로 둔다.
- [ ] [iop-cli-channel] Core/runtime이 IOP CLI를 통해 workspace agent authoring run을 시작하는 최소 통로를 정의한다. 검증: runtime이 workspace slot path, Plane 티켓 제목/본문, provider work item id, project sync 설정, roadmap 작성 지시를 CLI 실행 입력으로 전달할 수 있다.
- [ ] [authoring-state] Workspace agent authoring run의 최소 상태 모델을 정의한다. 검증: 첫 구현은 `in_progress`, `succeeded`, `failed`만 상태로 저장하고, stale/timeout은 별도 상태가 아니라 `updated_at` 기준으로 판정한다.
- [ ] [provider-identity] Plane 티켓과 workspace agent 결과를 연결하는 최소 identity를 정의한다. 검증: agent가 작성한 Milestone 작업 컨텍스트에 provider와 Plane work item id가 남고, sync layer는 이 identity로 pushed Milestone을 원래 Plane 티켓과 매칭한다. 별도 `authoring_run_id`는 첫 slice의 필수 매칭 키로 만들지 않는다.
- [ ] [agent-run-result] Workspace agent authoring run의 성공/실패 판정 기준을 정의한다. 검증: authoring 성공은 별도 구조화 응답이 아니라 `develop` branch에 Milestone 파일 변경이 commit/push된 상태로 판정하며, sync layer의 Plane work item id 검증은 `pushed-milestone-match` 단계에서 처리한다. 실패는 exit code, push 실패, dirty workspace, 충돌 상태로 구분된다.
- [ ] [agent-bridge-lock] Plane authoring 실행 단계의 선행 잠금을 정의한다. 검증: IOP CLI 1차 통로가 없으면 `plane-agent-authoring` 단계는 실행하지 않고, project sync 설정과 workspace slot 준비까지만 진행할 수 있다.
### Epic: [creation-sync] Milestone creation sync
Plane-origin 생성 경로를 `develop` agent-roadmap 기준의 `Todo` 검토 상태까지 수렴시킨다.
- [ ] [plane-trigger] Plane `Backlog + AGENT assignee` 상위 티켓을 Milestone 생성 trigger로 처리한다. 검증: NomadCode가 자신이 만든 후속 변경을 다시 사용자 trigger로 오인하지 않는 actor guard가 설명되어 있다.
- [ ] [plane-agent-authoring] Plane 티켓 제목/본문을 workspace agent authoring run의 입력으로 전달하는 경계를 정의한다. 검증: 정상 happy path에서 필수 agent/model 개입은 1회이며, 결과물은 별도 변환 응답이 아니라 workspace slot 안의 `agent-roadmap` Milestone 파일 작성/갱신과 `develop` commit/push다. 이 단계는 IOP CLI 1차 통로가 준비된 뒤에만 실행된다.
- [ ] [plane-draft] Plane 티켓 본문을 읽어 workspace agent가 roadmap skill로 Milestone 초안을 생성하거나 갱신하고 `develop`에 반영한다. 검증: 1상위 티켓은 1마일스톤으로 매핑되고 같은 Plane 티켓 재처리 시 중복 Milestone을 만들지 않는다. 이번 범위는 Plane-origin에 한정하되, Milestone 파일 작성과 push는 IDE-origin과 같은 workspace agent 경로를 쓴다.
- [ ] [develop-truth] `develop` branch의 agent-roadmap을 sync source of truth로 정의한다. 검증: feature branch나 로컬 초안은 Plane `Todo` projection 대상이 아니며, Plane `Todo` 티켓은 `develop`에 Milestone 변경이 commit/push된 뒤에만 생성/갱신된다.
- [ ] [pushed-milestone-match] Sync layer가 pushed commit과 Plane 티켓을 매칭하는 규칙을 정의한다. 검증: pushed commit 또는 roadmap scan에서 실제 `agent-roadmap/phase/*/milestones/*.md` 변경이 있고, 해당 Milestone에 provider와 Plane work item id가 확인될 때만 Plane 갱신 단계로 넘어간다.
- [ ] [plane-preserve] 원본 Plane 본문을 `사용자 요청:` 댓글로 보존한 뒤, `develop`에 반영된 Milestone 내용을 기준으로 Plane 본문을 치환한다. 검증: 댓글 보존 실패 시 본문 치환으로 넘어가지 않고, develop 반영 전에는 Todo 이동을 하지 않는다.
- [ ] [plane-todo] sync layer가 `develop` push 완료 또는 roadmap scan으로 감지한 Milestone을 기준으로 Plane 제목을 `[milestone-id] 제목` 형식으로 바꾸고 티켓을 `Todo`로 이동한다. 검증: milestone id는 `develop`의 Milestone 파일 slug를 기본값으로 쓰고, 실제 identity는 제목이 아니라 identity map의 Milestone path와 provider work item id로 추적된다.
- [ ] [creation-idmap] Milestone path, Milestone id, provider, work item id, provider revision, roadmap revision을 identity map으로 연결한다. 검증: 같은 Plane 티켓 또는 같은 Milestone path가 재처리되어도 같은 Milestone/work item pair를 찾을 수 있다.
- [ ] [creation-retry] 생성 동기화의 idempotency와 부분 실패 재시도 정책을 정한다. 검증: develop 반영, 댓글 보존, 본문 치환, 제목 변경, Todo 이동 중 일부만 성공해도 재시도 시 중복 생성 없이 남은 단계만 적용된다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 없음
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- Plane `Todo -> In Progress` 실행 시작
- Milestone Task를 Plane 하위 티켓으로 생성하거나 동기화하는 구현
- `agent-task/m-<milestone-id>` plan/code-review 실행 lifecycle
- 상위 티켓 `User Review`, `Done`, `Cancelled` 완료/폐기 처리
- 에이전트 챗 또는 IDE-origin Milestone 생성 후 Plane `Todo`로 projection하는 흐름
- Project sync 설정 관리 UI/UX 구현
- 사용자 승인 없는 archive 이동
## 작업 컨텍스트
- 관련 경로: `services/core/internal/workitem/`, `services/core/internal/scheduler/`, `services/core/internal/http/`, `services/core/internal/db/`, `services/core/migrations/`, `services/core/queries/`, `packages/contracts/`, `agent-roadmap/`
- 선행 작업: Roadmap Driven Agent-Ops Automation의 sync domain 방향 정리
- 후속 작업: `Milestone Execution Lifecycle Sync`
- 현재 지점: 이 마일스톤까지만 우선 진행하고 확인한다.
- Source of truth: `develop` branch의 `agent-roadmap`을 Milestone sync의 source of truth로 둔다. Plane은 이 마일스톤의 intake/projection/review UI이며, agent chat/IDE-origin 생성은 이번 범위에서 제외한다.
- Develop 반영 기준: 이 마일스톤에서 `develop`에 반영되었다는 말은 PR 요청이나 PR 생성이 아니라 `develop` branch에 Milestone 변경 commit/push가 완료된 상태를 뜻한다.
- Agent authoring 기준: Plane-origin 정상 흐름에서 필수 agent/model 개입은 Plane 티켓 제목/본문을 입력으로 workspace slot 안에서 Milestone 파일을 작성/갱신하고 `develop`에 commit/push하는 1회 authoring run이다. 이 단계의 결과물은 별도 응답 구조가 아니라 git에 반영된 roadmap 파일 변경이다.
- IOP CLI 통로 기준: workspace agent authoring run은 IOP CLI를 통해 실행하는 것을 1차 통로로 둔다. 이 통로가 준비되기 전에는 Plane authoring 실행 단계가 잠기며, project 설정/slot 준비와 계약 정리까지만 진행한다.
- Runtime 기준: Core/runtime은 project 설정 조회, slot 할당, checkout 준비, IOP CLI 실행 요청, actor guard, identity/idempotency, push 완료 감지, Plane 댓글/본문/제목/status 변경을 결정적으로 처리한다. 이번 범위는 Plane-origin에 한정하되, Milestone 파일 작성과 push는 IDE-origin과 같은 workspace agent 경로를 쓴다.
- Sync layer 기준: Plane `Backlog + AGENT assignee`는 authoring run 시작 trigger이고, Plane `Todo` 이동은 `develop` push 완료가 확인된 뒤 sync layer가 수행한다.
- Matching 기준: sync layer는 push 성공만으로 Plane을 갱신하지 않는다. pushed diff 또는 roadmap scan에서 Milestone 파일 변경을 확인하고, Milestone 작업 컨텍스트의 provider와 Plane work item id가 원래 Plane 티켓과 매칭된 뒤에만 댓글 보존/본문 치환/제목 변경/`Todo` 이동을 수행한다.
- Authoring 상태 기준: 첫 구현에서는 authoring run 상태를 `in_progress`, `succeeded`, `failed`로만 둔다. stale/timeout은 `updated_at`으로 판정하고, 별도 attempt 모델은 만들지 않는다.
- Project 설정: sync는 Plane project 단위로 묶으며, 각 project 설정은 Plane project target, git remote URL, source-of-truth branch, 실제 작업 workspace를 DB에 저장한다.
- Workspace 경로 기준: workspace base path는 NomadCode host 기준 절대 경로로 해석한다. 예를 들어 `/home/user/workspace`를 base로 두고 git repo 이름이 `nomadcode`이면 project workspace root는 `/home/user/workspace/nomadcode`다.
- Workspace slot 기준: 실제 작업공간은 project workspace root 아래 3자리 slot index로 둔다. 일반 상황에서는 `/home/user/workspace/nomadcode/000` 하나로 운영하고, 병렬 요청이 들어오면 `/home/user/workspace/nomadcode/001`, `/home/user/workspace/nomadcode/002`처럼 늘린다.
- Slot 사용 상태: slot은 Core DB에서 `available` 또는 `in_use` 같은 사용 상태를 추적한다. 여기서 사용 상태는 해당 slot이 현재 작업에 배정되어 다른 병렬 작업이 잡으면 안 되는지를 나타낸다.
- Checkout 기준: 안정성을 우선해 각 slot은 같은 git remote를 기준으로 독립 checkout을 갖는 작업공간으로 둔다. 초기 slot 생성 때 clone 비용이 들지만 이후 branch 이동과 일반 작업은 해당 slot 안에서 처리하므로 운영 비용을 낮춘다.
- 후속 UI/UX: project settings 화면에서 Plane project target, git remote, workspace 설정을 확인하고 수정하는 UX가 필요하다.
- 중지 지점: Plane-origin 생성이 Plane `Todo` 상위 티켓과 `develop` agent-roadmap Milestone identity로 연결된 상태다.
- 루프 확인: Plane `Backlog + AGENT assignee` -> IOP CLI로 workspace agent authoring run 실행 -> agent가 Milestone 파일 작성/갱신 후 `develop` push -> sync layer가 push 완료 또는 roadmap scan을 감지 -> Plane 원문 댓글 보존/본문 치환/제목 변경/`Todo` 이동 -> 사용자가 확인 후 `In Progress`로 이동하는 지점까지다. `In Progress` 이후 실행은 후속 잠금 범위다.
- 후속 잠금: `Todo -> In Progress` 이후 실행 lifecycle은 사용자가 명시적으로 잠금을 해제할 때까지 진행하지 않는다.
- Plane work item 경계: 이 마일스톤에서 말하는 Plane Milestone은 Plane native milestone object가 아니라 1상위 티켓 = 1 agent-roadmap Milestone으로 보는 work item mapping이다.
- 확인 필요: 없음

View file

@ -7,7 +7,7 @@
## 목표 ## 목표
NomadCode가 로드맵을 중심으로 사용자 입력, 실행 상태, 출력 검토, 완료 승인까지 한 흐름에서 운영하도록 Roadmap Operations Control Plane의 방향을 정한다. Plane은 사용자가 work item 상태를 움직이는 primary control UI이며, NomadCode Core는 Plane command를 검증해 roadmap/workflow/action side effect를 실행하고, agent-roadmap은 장기 원장으로 유지한다. 외부 에이전트는 MCP-first 제어 표면을 통해 Core를 호출하며, agent-ops 스킬은 문서 작성, 의미 해석, 변경 제안, MCP 호출 준비 계층으로 낮춘다. NomadCode가 로드맵을 중심으로 사용자 입력, 실행 상태, 출력 검토, 완료 승인까지 한 흐름에서 운영하도록 Roadmap Operations Control Plane의 방향을 정한다. Plane/Jira 같은 work item provider는 사용자가 작업 상태를 움직이는 primary control UI가 될 수 있으며, NomadCode Core는 provider command와 agent-roadmap 변경을 같은 sync domain에서 검증해 Milestone item과 work item이 동일한 구조로 수렴하도록 한다. agent-roadmap은 장기 원장으로 유지하고, 외부 에이전트는 MCP-first 제어 표면을 통해 Core를 호출하며, agent-ops 스킬은 문서 작성, 의미 해석, 변경 제안, MCP 호출 준비 계층으로 낮춘다.
## 상태 ## 상태
@ -29,8 +29,11 @@ NomadCode가 로드맵을 중심으로 사용자 입력, 실행 상태, 출력
- HTTP API와 MCP의 역할 분리 - HTTP API와 MCP의 역할 분리
- 사용자 입력, 출력 검토, 승인, 보완, archive 확인을 NomadCode 라인에서 처리하는 흐름 정의 - 사용자 입력, 출력 검토, 승인, 보완, archive 확인을 NomadCode 라인에서 처리하는 흐름 정의
- Plane 상위 티켓 1개를 Milestone 1개로 보고, Plane 하위 티켓을 Milestone 기능 Task로 투영하는 사용자 플로우 계약 정의 - Plane 상위 티켓 1개를 Milestone 1개로 보고, Plane 하위 티켓을 Milestone 기능 Task로 투영하는 사용자 플로우 계약 정의
- Plane/Jira 같은 provider work item과 agent-roadmap Milestone item 사이의 양방향 동기화 도메인 책임, 변경 감지, 수렴 정책 정의
- Plane/Jira provider project 단위로 sync를 묶고 provider project target, git remote, 실제 작업 workspace 설정을 Core DB 모델로 관리하는 기준 정의
- Plane-origin authoring에서 workspace agent를 IOP CLI 1차 통로로 실행하는 기준 정의
- Plane 상태 `Backlog`, `Todo`, `In Progress`, `User Review`, `Done`, `Cancelled`를 roadmap/agent-task lifecycle command로 해석하는 기준 정의 - Plane 상태 `Backlog`, `Todo`, `In Progress`, `User Review`, `Done`, `Cancelled`를 roadmap/agent-task lifecycle command로 해석하는 기준 정의
- `Todo + AGENT assignee`에서 Milestone 초안을 작성하고, `In Progress`에서 실제 실행과 하위 티켓 생성을 시작하는 gate 정의 - `Backlog + AGENT assignee`에서 Milestone 초안을 작성하고 Plane 티켓을 `Todo` 검토 상태로 옮긴 뒤, 사용자가 `In Progress`로 옮길 때 실제 실행과 하위 티켓 생성을 시작하는 gate 정의
- agent-ops roadmap skills를 작성/제안/MCP 호출 준비 계층으로 낮추는 방향 정의 - agent-ops roadmap skills를 작성/제안/MCP 호출 준비 계층으로 낮추는 방향 정의
- completion event, `USER_REVIEW.md`, 완료 리뷰, dependency lock을 Core action으로 다루는 기준 정의 - completion event, `USER_REVIEW.md`, 완료 리뷰, dependency lock을 Core action으로 다루는 기준 정의
@ -45,15 +48,32 @@ NomadCode 내부 core logic이 roadmap 기반 작업 운영을 소유하고, 외
- [ ] [mcp-tools] 외부 agent용 MCP tool 표면을 Core action wrapper로 정의하고 `dry_run`, `expected_revision`, `idempotency_key`, `actor`, `reason` 입력 원칙을 정한다. - [ ] [mcp-tools] 외부 agent용 MCP tool 표면을 Core action wrapper로 정의하고 `dry_run`, `expected_revision`, `idempotency_key`, `actor`, `reason` 입력 원칙을 정한다.
- [ ] [http-role] HTTP API는 Flutter UI, webhook, internal integration용 표면으로 유지하고, 외부 agent 제어는 MCP-first로 여는 기준을 정한다. - [ ] [http-role] HTTP API는 Flutter UI, webhook, internal integration용 표면으로 유지하고, 외부 agent 제어는 MCP-first로 여는 기준을 정한다.
- [ ] [review-gates] 사용자 입력, 출력 검토, `USER_REVIEW.md`, 완료 승인, archive 승인을 NomadCode workflow 안의 review gate로 처리하는 흐름을 정한다. - [ ] [review-gates] 사용자 입력, 출력 검토, `USER_REVIEW.md`, 완료 승인, archive 승인을 NomadCode workflow 안의 review gate로 처리하는 흐름을 정한다.
- [ ] [plane-control-flow] Plane을 primary control UI로 쓰는 lifecycle을 정리한다. 검증: `Backlog -> Todo -> In Progress -> User Review -> Done` 상태가 Milestone 초안, 실행 시작, 사용자 검토, 완료 archive로 어떻게 연결되는지 문서에서 일관되게 읽힌다. - [ ] [plane-control-flow] Plane을 primary control UI로 쓰는 lifecycle을 정리한다. 검증: `Backlog + AGENT assignee -> Todo -> In Progress -> User Review -> Done/Cancelled` 상태가 Milestone 초안, 사용자 검토, 실행 시작, 사용자 검토, 완료/archive 또는 폐기로 어떻게 연결되는지 문서에서 일관되게 읽힌다.
- [ ] [plane-identity] Plane 상위 티켓을 Milestone, 하위 티켓을 Milestone 기능 Task로 매핑하는 id 계약을 정한다. 검증: Milestone id는 파일명으로 유지하고, Plane work item id는 외부 provider id로 보존하는 기준이 명확하다. - [ ] [plane-identity] Plane 상위 티켓을 Milestone, 하위 티켓을 Milestone 기능 Task로 매핑하는 id 계약을 정한다. 검증: Milestone id는 파일명으로 유지하고, Plane work item id는 외부 provider id로 보존하는 기준이 명확하다.
- [ ] [todo-draft-gate] `Todo + AGENT assignee`를 Milestone 초안 생성/갱신 gate로 정의한다. 검증: Todo 진입이 자동 실행이 아니라 사용자 검토용 Milestone 작성 단계임이 명확하다. - [ ] [todo-draft-gate] `Backlog + AGENT assignee`를 Milestone 초안 생성/갱신 trigger로 정의하고, `develop` branch의 agent-roadmap에 반영된 뒤 Plane 티켓을 `Todo`로 이동하는 gate를 정한다. 검증: Todo 진입은 자동 실행이 아니라 `develop`에 존재하는 Milestone의 사용자 검토 단계임이 명확하다.
- [ ] [in-progress-exec-gate] 사용자가 Plane 상위 티켓을 `In Progress`로 옮길 때 실제 실행과 하위 티켓 전환을 시작하는 기준을 정한다. 검증: Milestone Task가 Plane 하위 티켓으로 생성되고 plan/code-review 루프로 들어가는 시점이 명확하다. - [ ] [in-progress-exec-gate] 사용자가 Plane 상위 티켓을 `In Progress`로 옮길 때 실제 실행과 하위 티켓 전환을 시작하는 기준을 정한다. 검증: Milestone Task가 Plane 하위 티켓으로 생성되고 plan/code-review 루프로 들어가는 시점이 명확하다.
- [ ] [cancelled-discard] 사용자가 Plane 상위 티켓을 `Cancelled`로 옮길 때 Milestone 초안 폐기 또는 보류를 처리하는 기준을 정한다. 검증: Plane 폐기 상태가 agent-roadmap의 `[폐기]` 또는 `[보류]` 중 어느 상태로 연결되는지와 사용자 승인 없는 archive 이동 금지가 설명된다.
- [ ] [child-task-review-policy] Milestone에서 만들어진 하위 작업은 막혔을 때만 `User Review`로 보내고, 정상 PASS 시 기본적으로 `Done`으로 보내는 정책을 정한다. 검증: 하위 작업의 기본 완료 흐름과 blocker review 흐름이 구분된다. - [ ] [child-task-review-policy] Milestone에서 만들어진 하위 작업은 막혔을 때만 `User Review`로 보내고, 정상 PASS 시 기본적으로 `Done`으로 보내는 정책을 정한다. 검증: 하위 작업의 기본 완료 흐름과 blocker review 흐름이 구분된다.
- [ ] [milestone-review-loop] 모든 하위 작업이 완료되면 상위 Milestone 티켓을 `User Review`로 보내고, 사용자가 `Done`으로 옮기면 완료/archive, `Todo`로 되돌리면 보완 루프로 처리하는 기준을 정한다. - [ ] [milestone-review-loop] 모든 하위 작업이 완료되면 상위 Milestone 티켓을 `User Review`로 보내고, 사용자가 `Done`으로 옮기면 완료/archive, `Todo`로 되돌리면 보완 루프로 처리하는 기준을 정한다.
- [ ] [skill-adapter] agent-ops roadmap skills가 action owner가 아니라 semantic authoring/proposal layer와 MCP call preparation layer가 되도록 축소 기준을 정한다. - [ ] [skill-adapter] agent-ops roadmap skills가 action owner가 아니라 semantic authoring/proposal layer와 MCP call preparation layer가 되도록 축소 기준을 정한다.
- [ ] [first-slice] 첫 구현 단위를 `roadmap.validate`, `roadmap.get_position`, `roadmap.ingest_completion_event` 중 어떤 순서로 자를지 정한다. - [ ] [first-slice] 첫 구현 단위를 `roadmap.validate`, `roadmap.get_position`, `roadmap.ingest_completion_event` 중 어떤 순서로 자를지 정한다.
### Epic: [sync-domain] Milestone/work item sync domain
Plane/Jira 같은 provider work item과 agent-roadmap Milestone item을 동일한 작업 구조로 유지하는 별도 Core sync domain을 정의한다.
- [ ] [sync-owner] sync domain의 소유 책임을 정의한다. 검증: provider adapter는 native API read/write만 담당하고, Milestone/work item identity mapping, revision 비교, 수렴 적용은 Core sync domain 책임으로 읽힌다.
- [ ] [sync-identity] Milestone, Epic/Task item id, provider work item id, parent-child 관계, provider revision, roadmap revision을 묶는 identity 계약을 정한다. 검증: Plane/Jira 어느 쪽에서 시작해도 같은 Milestone item 구조로 매핑되는 기준이 명확하다.
- [ ] [sync-project-config] provider project 단위 sync 설정 모델과 저장 경계를 정한다. 검증: provider project target, git remote URL, source-of-truth branch, workspace id/path가 Core DB에 저장되는 project sync 설정으로 정의되고, 설정 누락 또는 중복 시 생성 동기화를 진행하지 않는 기준이 있다.
- [ ] [sync-detection] provider webhook, provider polling, roadmap file/index scan, agent-task completion event를 같은 변경 감지 입력으로 정리한다. 검증: 양쪽 중 어느 쪽에서 변경이 발생해도 sync event로 정규화되는 흐름이 문서화되어 있다.
- [ ] [sync-convergence] 한쪽 변경을 다른 쪽에 반영해 Milestone item과 work item이 동일 형태로 수렴하는 apply 정책을 정한다. 검증: 생성, 제목/설명, 상태, parent-child, 완료/검토 흐름이 양방향으로 어떻게 반영되는지 설명된다.
- [ ] [sync-conflict] 동시 수정, revision mismatch, 삭제/archive, 사용자 승인 필요 변경의 conflict 처리 정책을 정한다. 검증: Core가 조용히 덮어쓰지 않고 review gate 또는 dry-run proposal로 멈추는 기준이 있다.
- [ ] [sync-schedule] sync domain의 scheduler 책임을 정한다. 검증: webhook이 없는 provider나 누락 이벤트를 주기 polling으로 보정하고, 같은 idempotency/revision 계약을 쓰는 방향이 명확하다.
- [ ] [sync-contract] `packages/contracts`에 sync event/action 후보를 남긴다. 검증: `roadmap_sync.inspect`, `roadmap_sync.apply`, `roadmap_sync.changed` 같은 후보 표면과 필수 입력 필드가 compatibility note에 정리되어 있다.
- [ ] [plane-origin-flow] Plane-origin Milestone 생성 시나리오를 정한다. 검증: Backlog 티켓 본문을 입력으로 IOP CLI 1차 통로를 통해 실행된 workspace agent가 같은 Milestone 파일 작성/push 경로에서 roadmap skill로 Milestone 초안을 만들고, `develop` 반영 후 sync layer가 pushed Milestone의 provider/work item identity를 검증한 뒤 원본 본문은 `사용자 요청:` 댓글로 보존하며, Plane 본문은 `develop`의 Milestone 내용으로 치환하고, 제목은 `[milestone-id] 제목` 형식으로 바꾼 뒤 Todo로 이동하는 순서가 문서화되어 있다.
- [ ] [agent-origin-flow] Agent-origin Milestone 생성 시나리오를 정한다. 검증: 에이전트 대화로 생성된 Milestone이 `develop` branch에 반영되었을 때 Plane parent ticket을 Todo 상태로 생성하고 identity map을 연결하는 흐름이 문서화되어 있다.
- [ ] [sync-idempotency] Plane-origin과 Agent-origin 생성의 중복 방지와 부분 실패 복구 정책을 정한다. 검증: 같은 Plane 티켓 또는 같은 Milestone path가 재처리되어도 중복 Milestone/티켓을 만들지 않고, 댓글 보존/본문 치환/제목 변경/상태 이동 중 실패한 단계를 재시도할 수 있다.
## 완료 리뷰 ## 완료 리뷰
- 상태: 없음 - 상태: 없음
@ -68,25 +88,54 @@ NomadCode 내부 core logic이 roadmap 기반 작업 운영을 소유하고, 외
- IOP 내부 모델 라우팅, MCP/tool policy, output validation, RAG, context compression 구현 - IOP 내부 모델 라우팅, MCP/tool policy, output validation, RAG, context compression 구현
- 전체 Flutter UI 구현 - 전체 Flutter UI 구현
- Plane/Jira/Mattermost provider projection 세부 구현, Plane webhook/하위 티켓 생성 adapter 실제 구현 - Project sync 설정 관리 UI/UX 실제 구현
- Plane/Jira/Mattermost provider projection 세부 구현, Plane webhook/하위 티켓 생성 adapter 실제 구현. 단, Milestone/work item sync domain의 provider-neutral 책임과 계약 후보 정의는 포함한다.
- 사용자 승인 없는 자동 archive 이동 - 사용자 승인 없는 자동 archive 이동
- agentic-framework 공통 스킬의 실제 개편 - agentic-framework 공통 스킬의 실제 개편
## 작업 컨텍스트 ## 작업 컨텍스트
- 관련 경로: `services/core/internal/workflow/`, `services/core/internal/scheduler/`, `services/core/internal/http/`, `packages/contracts/`, `agent-roadmap/`, `agent-ops/skills/common/update-roadmap/SKILL.md`, `agent-ops/skills/common/plan/SKILL.md`, `agent-ops/skills/common/code-review/SKILL.md` - 관련 경로: `services/core/internal/workflow/`, `services/core/internal/scheduler/`, `services/core/internal/http/`, `services/core/internal/workitem/`, `services/core/internal/db/`, `services/core/migrations/`, `services/core/queries/`, `packages/contracts/`, `agent-roadmap/`, `agent-ops/skills/common/update-roadmap/SKILL.md`, `agent-ops/skills/common/plan/SKILL.md`, `agent-ops/skills/common/code-review/SKILL.md`
- 표준선(선택): Plane은 사용자의 primary control UI이고, Plane 상태 변화는 NomadCode Core가 검증해 실행하는 command로 본다. NomadCode Core는 roadmap/action side effect와 idempotency/revision 검증을 소유하고, agent-roadmap은 장기 원장으로 유지한다. agent-ops 스킬은 사용자의 자연어와 문서 초안을 Core/MCP 호출 입력으로 정리한다. - 표준선(선택): `develop` branch의 `agent-roadmap`을 Milestone sync의 source of truth로 둔다. Plane/Jira 같은 work item provider는 사용자의 primary control UI와 projection 표면이 될 수 있고, provider 상태 변화와 agent-roadmap 변경은 NomadCode Core sync domain이 검증해 실행하는 command로 본다. NomadCode Core는 roadmap/action side effect, Milestone/work item identity mapping, idempotency/revision 검증, conflict review gate를 소유하고, agent-ops 스킬은 사용자의 자연어와 문서 초안을 Core/MCP 호출 입력으로 정리한다.
- 프로젝트 설정 기준: Sync는 Plane/Jira provider project 단위로 묶으며, 각 project sync 설정은 provider project target, git remote URL, source-of-truth branch, 실제 작업 workspace를 Core DB에 저장한다. 이 설정은 이후 Project settings UI/UX에서 확인/수정할 수 있어야 한다.
- 로컬 제어 표면 후보: `http://127.0.0.1:8080/v1` - 로컬 제어 표면 후보: `http://127.0.0.1:8080/v1`
- 선행 작업: Workflow Core, External Integration, Project Workspace Management UX, 로드맵 스킬 운영 복잡도 평가 - 선행 작업: Workflow Core, External Integration, Project Workspace Management UX, 로드맵 스킬 운영 복잡도 평가
- 후속 작업: Roadmap Operations Control Plane 첫 구현 slice, MCP tool contract 작성 - 후속 작업: `Milestone Work Item Creation Sync`, MCP tool contract 작성
- 현재 지점: 사용자 결정에 따라 현재 작업 후보에서 제외하고 로드맵 마지막 Phase로 이동했다. Workflow Core와 외부 통합, 프로젝트 제어 UX가 먼저 닫힌 뒤 roadmap/action core 책임과 MCP-first 제어 표면을 구현 가능한 Core action 후보로 정리한다. - 현재 지점: Agent-Ops MCP Control Plane이 진행중 Phase가 되었고, 실제 구현은 `Milestone Work Item Creation Sync` slice부터 시작한다. 이 마일스톤은 roadmap/action core 책임, Milestone/work item sync domain, MCP-first 제어 표면을 구현 가능한 Core action 후보로 정리하는 상위 설계/계약 문서다.
- 실행 경계: 이 마일스톤은 상위 방향과 계약 정리용이다. 실제 구현은 `Milestone Work Item Creation Sync`부터 진행하고, `Milestone Execution Lifecycle Sync`는 사용자가 해제할 때까지 잠근다.
- 동기화 도메인 방향:
- Sync domain은 provider adapter보다 위에 위치하며, provider native DTO를 직접 소유하지 않는다.
- Sync domain의 Milestone source of truth는 `develop` branch의 `agent-roadmap`이다. Plane/Jira work item은 intake/projection/review UI로 본다.
- Sync domain은 provider project 단위 project sync 설정을 먼저 해석한 뒤 git repository와 workspace를 선택한다.
- Plane-origin authoring은 IOP CLI 1차 통로로 workspace agent를 실행하며, 이 통로가 없으면 Plane authoring 단계는 잠긴다.
- Sync domain은 `Milestone <-> provider parent work item`, `Milestone Task <-> provider child work item` 관계를 같은 identity map으로 관리한다.
- 변경 감지는 provider webhook을 우선하되, 누락/미지원 provider는 scheduler polling으로 보정한다.
- agent-roadmap 문서 변경, provider work item 변경, agent-task completion event는 모두 같은 provider-neutral sync event로 정규화한다.
- 양쪽 변경이 충돌하면 조용히 덮어쓰지 않고 expected revision mismatch로 멈춘 뒤 사용자 review gate 또는 dry-run proposal로 올린다.
- 용어 경계: Plane `User Review`는 provider board state이고, agent-task `USER_REVIEW.md`는 자동 plan/code-review 루프를 멈추는 내부 stop artifact다. 둘은 모두 사용자 판단 신호지만 저장 위치와 트리거가 다르다. - 용어 경계: Plane `User Review`는 provider board state이고, agent-task `USER_REVIEW.md`는 자동 plan/code-review 루프를 멈추는 내부 stop artifact다. 둘은 모두 사용자 판단 신호지만 저장 위치와 트리거가 다르다.
- Plane 제어 플로우: - Plane 제어 플로우:
- `Backlog`: 아이디어/후보이며 아직 Milestone 확정이 아니다. - `Backlog`: 사용자가 해야 할 일을 Plane 상위 티켓으로 작성하는 아이디어/요청 상태다.
- `Todo + AGENT assignee`: NomadCode가 Plane 티켓 내용을 바탕으로 Milestone 초안을 생성하거나 갱신한다. 원본 사용자 요청은 Plane comment 히스토리로 보존하고, Milestone 본문에는 정제된 목표/범위/기능/작업 컨텍스트를 둔다. - `Backlog + AGENT assignee`: 실제 등록된 agent user에게 티켓이 할당되면 NomadCode가 Plane 티켓 본문을 읽고, project sync 설정으로 선택한 workspace slot 안에서 IOP CLI 1차 통로로 workspace agent를 실행해 roadmap skill로 Milestone 파일을 생성하거나 갱신한다.
- Milestone 초안 생성 후: NomadCode는 Plane 티켓 본문을 기반으로 workspace agent가 Milestone 파일을 만들게 하되, `develop` branch의 agent-roadmap에 commit/push되기 전까지 Plane `Todo` projection 대상으로 보지 않는다.
- `develop` 반영 후: NomadCode는 원본 사용자 본문을 Plane 댓글에 `사용자 요청:` prefix로 보존하고, Plane 본문을 `develop`에 반영된 Milestone 내용으로 치환하며, 제목을 `[milestone-id] 제목` 형식으로 바꾼 뒤 티켓을 `Todo`로 옮긴다. 여기서 `milestone-id``develop`의 Milestone 파일 slug를 기본값으로 쓴다.
- `Todo`: 사용자가 `develop`에 존재하는 Milestone 내용을 Plane에서 검토하는 상태다. 자동 실행 gate가 아니며, 사용자는 작업 착수 시 `In Progress`, 폐기 시 `Cancelled`로 옮긴다.
- `In Progress`: 사용자가 Milestone 초안을 검토한 뒤 실행을 승인한 상태다. NomadCode는 Milestone 기능 Task를 Plane 하위 티켓으로 전환하고 `agent-task/m-<milestone-id>` plan/code-review 루프를 시작한다. - `In Progress`: 사용자가 Milestone 초안을 검토한 뒤 실행을 승인한 상태다. NomadCode는 Milestone 기능 Task를 Plane 하위 티켓으로 전환하고 `agent-task/m-<milestone-id>` plan/code-review 루프를 시작한다.
- 하위 티켓: Milestone에서 만들어진 작업은 정상 PASS 시 기본적으로 `Done`으로 이동한다. 작업 중 사용자 결정, 외부 환경, 범위 충돌처럼 자동 진행이 막힐 때만 `User Review`로 이동한다. - 하위 티켓: Milestone에서 만들어진 작업은 정상 PASS 시 기본적으로 `Done`으로 이동한다. 작업 중 사용자 결정, 외부 환경, 범위 충돌처럼 자동 진행이 막힐 때만 `User Review`로 이동한다.
- `User Review`: 상위 Milestone 티켓은 모든 하위 작업이 완료되었거나 사용자 판단이 필요한 경우 이동한다. 사용자가 `Done`으로 옮기면 `[완료]`와 archive를 수행하고, `Todo`로 되돌리면 내부 Milestone을 `[진행중]`/보완 필요로 되돌려 추가 요구사항을 반영하는 루프로 처리한다. - `User Review`: 상위 Milestone 티켓은 모든 하위 작업이 완료되었거나 사용자 판단이 필요한 경우 이동한다. 사용자가 `Done`으로 옮기면 `[완료]`와 archive를 수행하고, `Todo`로 되돌리면 내부 Milestone을 `[진행중]`/보완 필요로 되돌려 추가 요구사항을 반영하는 루프로 처리한다.
- `Done`: 사용자 최종 완료 승인이다. - `Done`: 사용자 최종 완료 승인이다.
- `Cancelled`: 폐기 또는 중단 루트다. - `Cancelled`: 폐기 또는 중단 루트다.
- Agent-origin 생성 플로우:
- 에이전트와의 대화 중 사용자가 Milestone 생성을 요청하면 현재 roadmap 규칙에 따라 프로젝트 안에 Milestone 문서를 만든다.
- 생성된 Milestone이 `develop` branch에 반영되면 sync domain은 해당 Milestone을 Plane 상위 티켓으로 생성하고 `Todo` 상태에 둔다.
- Plane 티켓 제목은 `[milestone-id] 제목` 형식을 사용하고, 본문은 Milestone 내용으로 채우며, identity map에는 Milestone path와 Plane work item id를 함께 저장한다. 여기서 `milestone-id`는 Milestone 파일 slug를 기본값으로 쓴다.
- 시나리오 점검:
- Agent user assignment가 trigger이므로 NomadCode가 자기 자신이 만든 상태/본문 변경을 다시 trigger로 오인하지 않도록 actor guard와 idempotency key가 필요하다.
- 원본 본문을 댓글로 보존한 뒤 본문을 치환하므로, 댓글 생성 실패 후 본문 치환이 진행되지 않게 단계별 apply 순서와 재시도 정책이 필요하다.
- Plane-origin은 별도 구조화 응답을 받아 적용하는 방식이 아니라, IDE-origin과 같은 workspace agent 파일 작성 및 push 경로를 사용한다.
- Plane `Todo` 이동은 workspace agent 실행 직후가 아니라 `develop` push 완료를 sync layer가 감지한 뒤 수행한다.
- Sync layer는 push 성공만으로 Plane을 갱신하지 않고, pushed Milestone의 provider/work item identity가 원래 Plane 티켓과 매칭되는지 확인한다.
- `[milestone-id] 제목`의 milestone id는 `develop`의 Milestone 파일 slug를 기본값으로 고정한다. 제목 변경만으로 identity를 추적하지 않는다.
- Agent-origin sync는 feature branch 초안이 아니라 `develop` branch에 반영된 Milestone만 Plane에 Todo 티켓으로 만든다.
- Plane-origin과 Agent-origin 모두 project sync 설정을 통해 provider project, git repository, workspace를 확정하며, 설정이 없거나 둘 이상이면 생성 동기화를 멈추고 사용자 설정 또는 운영자 조치를 요구한다.
- Cancelled는 사용자 폐기 신호지만 archive 이동은 사용자 승인 경계를 지켜야 하므로, 우선 `[폐기]` 후보 또는 review gate로 처리한다.
- 확인 필요: 없음 - 확인 필요: 없음

View file

@ -21,15 +21,15 @@ Flutter-first 클라이언트 구조 위에서 `apps/client` 모듈, web/mobile
- 경로: `agent-roadmap/phase/project-workspace-management-ux/milestones/project-workspace-management-ux.md` - 경로: `agent-roadmap/phase/project-workspace-management-ux/milestones/project-workspace-management-ux.md`
- 요약: 프로젝트 단위 제어 UX의 desktop/mobile 역할, parity 기준, IOP/task 결과 표시 기준을 정리하는 현재 작업 지점이다. - 요약: 프로젝트 단위 제어 UX의 desktop/mobile 역할, parity 기준, IOP/task 결과 표시 기준을 정리하는 현재 작업 지점이다.
- [계획] Workbench Shell and Optional IOP Composition - [계획] Workbench Provider Slot Composition
- 경로: `agent-roadmap/phase/project-workspace-management-ux/milestones/workbench-shell-optional-iop-composition.md` - 경로: `agent-roadmap/phase/project-workspace-management-ux/milestones/workbench-provider-slot-composition.md`
- 요약: 상단 titlebar와 우측 icon rail 기반 workbench를 만들고, IOP 관리 UI를 선택적 Flutter package composition으로 중앙 content에 삽입하거나 빼도 빌드 가능한 구조로 정리한다. - 요약: 상단 titlebar와 우측 icon rail 기반 workbench를 유지하면서 IOP/OTO 같은 외부 Flutter console을 provider slot으로 등록, 삽입, 제거할 수 있는 구조로 정리한다.
## Phase 경계 ## Phase 경계
- 기존 Project/Session/Workspace 방향을 대체하지 않고 보강한다. - 기존 Project/Session/Workspace 방향을 대체하지 않고 보강한다.
- 실제 앱 UX 구현과 상세 화면 작업은 공통 모듈, workflow/core 안정화, 외부 통합 기준이 먼저 정리된 뒤 후순위로 진행한다. - 실제 앱 UX 구현과 상세 화면 작업은 공통 모듈, workflow/core 안정화, 외부 통합 기준이 먼저 정리된 뒤 후순위로 진행한다.
- IOP 내부 모델 라우팅, 외부 agent 추가 흐름, 상세 화면/API/DB schema 확정은 이 Phase의 결정 전 범위에서 제외한다. - IOP/OTO 내부 모델 라우팅, 외부 agent 추가 흐름, 상세 화면/API/DB schema 확정은 이 Phase의 결정 전 범위에서 제외한다.
- IOP 관리 화면 자체는 IOP package가 소유하고, NomadCode는 우측 rail host와 optional composition adapter만 소유한다. - IOP/OTO 같은 외부 제품 관리 화면 자체는 각 제품 package가 소유하고, NomadCode는 우측 rail host와 provider slot composition adapter만 소유한다.
- UX 책임 경계와 우선순위는 사용자 결정이 필요한 잠금 항목으로 유지한다. - UX 책임 경계와 우선순위는 사용자 결정이 필요한 잠금 항목으로 유지한다.
- `../iop``../alt`로 복제할 내용은 제품 feature 전체가 아니라 client skeleton, integration boundary, bootstrap convention, 문서화된 handoff 기준으로 제한한다. - `../iop``../alt`로 복제할 내용은 제품 feature 전체가 아니라 client skeleton, integration boundary, bootstrap convention, 문서화된 handoff 기준으로 제한한다.

View file

@ -28,7 +28,7 @@ Flutter-first 클라이언트 구조 위에서 기존 Project/Session/Workspace
- Flutter 앱 desktop 프로젝트 단위 새창 구조 정리 - Flutter 앱 desktop 프로젝트 단위 새창 구조 정리
- mobile 프로젝트 관리 상단탭 구조 정리 - mobile 프로젝트 관리 상단탭 구조 정리
- code-server, Agent Chat, task/execution state, 파일 변경 결과, diff/branch/commit/PR 흐름을 프로젝트 제어 화면에 배치하는 기준 정리 - code-server, Agent Chat, task/execution state, 파일 변경 결과, diff/branch/commit/PR 흐름을 프로젝트 제어 화면에 배치하는 기준 정리
- NomadCode Project/Session/Workspace 상태와 IOP 호출 결과를 사용자에게 보여주는 경계 정리 - NomadCode Project/Session/Workspace 상태와 IOP 호출 결과를 사용자에게 보여주는 경계 정리'
## 기능 ## 기능

View file

@ -0,0 +1,95 @@
# Milestone: Workbench Provider Slot Composition
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/project-workspace-management-ux/PHASE.md`
## 목표
NomadCode Flutter 앱의 상단 titlebar와 우측 icon rail 기반 workbench shell을 유지하면서, IOP 전용 `iopContent` 같은 1회성 slot을 provider 기반 slot registry로 추상화한다. IOP와 OTO는 각각 provider adapter로 등록되어 우측 rail item과 중앙 content에 삽입될 수 있고, 기본 NomadCode client는 외부 제품 package 없이도 독립적으로 빌드되어야 한다.
## 상태
[계획]
## 구현 잠금
- 상태: 해제
- 결정 필요: 없음
## 범위
- NomadCode desktop/workbench shell의 상단 titlebar + 우측 icon rail + center content 구조 유지
- 기존 IOP 전용 slot을 provider slot registry와 provider descriptor/adapter 경계로 일반화
- IOP, OTO 같은 외부 Flutter console package를 선택적 provider로 등록하는 구조
- provider별 rail item, placeholder, center content, agent capability pack 조립 경계
- 기본 `apps/client`가 IOP/OTO package를 import하지 않고도 빌드되는 dependency boundary
- 포함 빌드 또는 별도 composition package가 IOP/OTO provider를 주입하는 방식
## 기능
### Epic: [workbench-shell] NomadCode right-rail workbench
NomadCode는 여러 도메인 관리 화면을 우측 icon rail과 중앙 content host로 전환하는 제품 shell을 소유한다.
- [ ] [titlebar] NomadCode shell은 상단 titlebar 영역을 제공하고, 앱/워크스페이스/현재 컨텍스트 상태를 이 영역에서 표현한다.
- [ ] [right-rail] NomadCode shell은 VS Code activity bar처럼 아이콘만 있는 우측 rail을 제공한다.
- [ ] [center-content] 우측 rail item 선택에 따라 중앙 content가 Agent, Workflow, Web Context, Settings, provider slot 등으로 전환된다.
- [ ] [layout-shape] 상단 titlebar와 우측 rail이 ㄱ자 구조를 만들고, 중앙 content는 host 도메인 화면을 침범 없이 표시한다. 검증: narrow/wide layout에서 titlebar, rail, center content가 겹치지 않는다.
### Epic: [provider-slots] Provider slot registry
Workbench는 IOP/OTO 같은 제품별 console을 하드코딩된 enum/field가 아니라 provider descriptor 목록으로 조립한다.
- [ ] [slot-contract] provider slot descriptor는 id, label, icon, center content builder, optional placeholder, optional agent capability pack 주입 지점을 갖는다.
- [ ] [slot-registry] NomadCode workbench shell은 기본 section과 provider slot을 하나의 rail model로 합성한다. 검증: IOP/OTO slot을 추가해도 shell enum과 switch에 제품별 분기가 늘어나지 않는다.
- [ ] [provider-adapter] 각 provider adapter는 외부 console package public API와 NomadCode slot contract 사이만 연결한다.
- [ ] [dependency-boundary] 기본 `apps/client` dependency graph에는 IOP/OTO package import가 없다. 검증: provider package가 없는 환경에서도 기본 client analyze/build가 실패하지 않는다.
### Epic: [agent-surface] Shared agent shell composition
채팅형 agent shell은 NomadCode, IOP, OTO가 함께 쓰는 공통 package로 두고, 각 제품은 capability pack만 등록한다.
- [ ] [agent-shell-package] 메시지, tool call, 승인 UI, capability registry를 포함한 공통 `agent_shell` package를 NomadCode 제품 shell에서 사용할 수 있다.
- [ ] [nomad-agent] NomadCode agent capability는 WebView 화면 관찰/조작, workflow/task/workspace, project context 같은 NomadCode 책임 범위만 제공한다.
- [ ] [provider-agent-pack] IOP/OTO 포함 빌드는 같은 agent shell registry에 각 provider의 agent capability pack을 추가 등록할 수 있다.
### Epic: [provider-embeds] IOP and OTO native Flutter embeds
IOP와 OTO 관리는 webview나 별도 앱이 아니라 각 repository의 embeddable Flutter console package를 provider slot으로 삽입한다.
- [ ] [iop-provider] IOP 포함 빌드는 `iop_console` 또는 동등한 IOP-owned package를 provider slot으로 등록하고, 기존 IOP placeholder를 native Flutter console로 대체한다.
- [ ] [oto-provider] OTO 포함 빌드는 `oto_console` package를 provider slot으로 등록하고, 우측 rail에 OTO slot을 추가해 중앙 content에 OTO console을 표시한다.
- [ ] [host-config] NomadCode는 provider별 endpoint/auth/token reference/theme/navigation callback을 주입하고, provider package 내부 state와 NomadCode 제품 state를 느슨하게 연결한다.
- [ ] [remove-cleanly] IOP 또는 OTO provider를 제거해도 NomadCode Agent, Workflow, Web Context, Settings rail item과 중앙 content 전환은 유지된다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- IOP/OTO 관리 UI 자체 구현과 각 제품의 Control Plane/Runner/Edge 운영 책임
- IOP/OTO 내부 모델 라우팅, adapter/target 실행 정책, runtime 최적화 구현
- NomadCode default app에 IOP/OTO package를 직접 의존시키는 구조
- 런타임 동적 plugin framework 도입
- WebView를 통해 IOP/OTO 화면을 띄우는 대체 구현
## 작업 컨텍스트
- 관련 경로: `apps/client/`, `packages/contracts/`, `services/core/`
- 외부 관련 경로: `../iop/apps/client/`, `../iop/packages/flutter/iop_console/`, `../oto/apps/client/`, `../oto/packages/flutter/oto_console/`, `../agent-shell/`
- 표준선(선택): NomadCode shell은 상단 titlebar + 우측 icon rail + center content 구조를 소유한다.
- 표준선(선택): 외부 제품 UI는 provider adapter/composition layer에서만 import하고, default build는 외부 제품 package 없이 빌드 가능해야 한다.
- 표준선(선택): Agent shell은 공통 package, NomadCode/IOP/OTO는 각각 capability pack으로 조립한다.
- 선행 작업: Client Integration Standardization, Project Workspace Management UX
- 선행 확인 대상: `../iop/packages/flutter/iop_console/`, `../oto/packages/flutter/oto_console/`
- 후속 작업: 실제 desktop/mobile project workspace 화면 구현, workflow/provider 관리 탭별 세부 content 구현
- 확인 필요: 후속 구현 계획에서 app/flavor/package 중 어떤 optional composition 방식을 택할지 확정한다.

View file

@ -1,94 +0,0 @@
# Milestone: Workbench Shell and Optional IOP Composition
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/project-workspace-management-ux/PHASE.md`
## 목표
NomadCode Flutter 앱을 상단 titlebar와 우측 icon rail이 만드는 ㄱ자 workbench shell로 정리하고, rail item 선택에 따라 중앙 content가 바뀌는 구조를 만든다. IOP 관리는 optional feature composition으로 두어 포함 빌드에서는 IOP의 native Flutter 관리 UI가 그대로 중앙 content에 들어오고, 기본 빌드에서는 IOP 의존성 없이 NomadCode가 독립적으로 빌드되어야 한다.
## 상태
[계획]
## 구현 잠금
- 상태: 해제
- 결정 필요: 없음
## 범위
- NomadCode desktop/workbench shell의 상단 titlebar + 우측 icon rail + center content 구조
- Agent, Workflow, IOP 관리, Web Context, Settings 같은 도메인 rail item 선택과 중앙 content 전환
- 공통 agent shell package를 NomadCode agent와 IOP agent capability가 함께 사용할 수 있는 조립 경계
- IOP 포함 빌드와 IOP 제외 기본 빌드를 분리하는 optional composition package/app 경계
- IOP 관리 페이지가 IOP repository의 embeddable console package를 native Flutter widget으로 그대로 삽입하는 구조
- NomadCode가 독립 제품으로 움직일 때 IOP feature import 없이 빌드 가능한 구조
## 기능
### Epic: [workbench-shell] NomadCode right-rail workbench
NomadCode는 여러 도메인 관리 화면을 우측 icon rail과 중앙 content host로 전환하는 제품 shell을 소유한다.
- [ ] [titlebar] NomadCode shell은 상단 titlebar 영역을 제공하고, 앱/워크스페이스/현재 컨텍스트 상태를 이 영역에서 표현한다.
- [ ] [right-rail] NomadCode shell은 VS Code activity bar처럼 아이콘만 있는 우측 rail을 제공한다.
- [ ] [center-content] 우측 rail item 선택에 따라 중앙 content가 Agent, Workflow, IOP 관리, Web Context, Settings 등으로 전환된다.
- [ ] [layout-shape] 상단 titlebar와 우측 rail이 ㄱ자 구조를 만들고, 중앙 content는 host 도메인 화면을 침범 없이 표시한다. 검증: narrow/wide layout에서 titlebar, rail, center content가 겹치지 않는다.
### Epic: [agent-surface] Shared agent shell composition
채팅형 agent shell은 NomadCode와 IOP가 함께 쓰는 공통 package로 두고, 각 제품은 capability pack만 등록한다.
- [ ] [agent-shell-package] 메시지, tool call, 승인 UI, capability registry를 포함한 공통 agent shell package를 NomadCode 제품 shell에서 사용할 수 있다.
- [ ] [nomad-agent] NomadCode agent capability는 WebView 화면 관찰/조작, workflow/task/workspace, project context 같은 NomadCode 책임 범위만 제공한다.
- [ ] [iop-agent-slot] IOP 포함 빌드는 같은 agent shell registry에 IOP agent capability pack을 추가 등록할 수 있다.
### Epic: [optional-iop] Optional IOP feature composition
IOP 관리는 NomadCode default app에 직접 박지 않고 별도 composition layer로 격리한다.
- [ ] [default-build] 기본 `apps/client` 빌드는 IOP package를 import하지 않고 NomadCode 기능만으로 빌드된다. 검증: IOP package가 없는 환경에서도 기본 client build/analyze가 실패하지 않는다.
- [ ] [iop-build] IOP 포함 빌드는 별도 app/flavor/composition package에서 IOP console package를 import하고 우측 rail의 IOP item에 연결한다.
- [ ] [feature-adapter] `nomadcode_iop_feature` 또는 동등한 adapter는 NomadCode feature registry와 IOP console public API 사이만 연결한다.
- [ ] [dependency-boundary] IOP 의존 import는 optional composition layer에만 존재하고 NomadCode core shell/domain package로 새지 않는다. 검증: default app dependency graph에서 IOP package가 제외된다.
### Epic: [iop-embed] Native Flutter IOP management embed
NomadCode의 IOP 관리 탭은 IOP에서 사용하는 화면을 webview나 별도 앱이 아니라 native Flutter widget으로 중앙 content에 삽입한다.
- [ ] [iop-center-page] 우측 rail의 IOP item 선택 시 중앙 content에 IOP embeddable console이 표시된다.
- [ ] [same-ui] NomadCode에 포함된 IOP 관리 화면과 IOP standalone 앱의 관리 화면은 같은 IOP console package를 사용한다.
- [ ] [host-config] NomadCode는 IOP endpoint/auth/token reference/theme/navigation callback을 주입하고 IOP package 내부 state와 NomadCode 제품 state를 느슨하게 연결한다.
- [ ] [remove-cleanly] IOP feature를 제거해도 NomadCode Agent, Workflow, Web Context, Settings rail item과 중앙 content 전환은 유지된다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- IOP 관리 UI 자체 구현과 IOP Control Plane/Edge/Node 운영 책임
- IOP 내부 모델 라우팅, adapter/target 실행 정책, runtime 최적화 구현
- NomadCode default app에 IOP package를 직접 의존시키는 구조
- 런타임 동적 plugin framework 도입
- WebView를 통해 IOP 화면을 띄우는 대체 구현
## 작업 컨텍스트
- 관련 경로: `apps/client/`, `packages/contracts/`, `services/core/`
- 외부 관련 경로: `../iop/apps/client/`, `../iop/packages/`
- 표준선(선택): NomadCode shell은 상단 titlebar + 우측 icon rail + center content 구조를 소유한다.
- 표준선(선택): IOP 관리는 optional composition layer에서만 import하고, default build는 IOP 없이 빌드 가능해야 한다.
- 표준선(선택): Agent shell은 공통 package, NomadCode/IOP는 각각 capability pack으로 조립한다.
- 선행 작업: Client Integration Standardization, Project Workspace Management UX, IOP Embeddable Console Workbench
- 후속 작업: 실제 desktop/mobile project workspace 화면 구현, workflow/iop 관리 탭별 세부 content 구현
- 확인 필요: 후속 구현 계획에서 app/flavor/package 중 어떤 optional composition 방식을 택할지 확정한다.

View file

@ -0,0 +1,184 @@
<!-- task=m-milestone-work-item-creation-sync/01_project_sync_store plan=1 tag=API -->
# 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`. 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-06
task=m-milestone-work-item-creation-sync/01_project_sync_store, plan=1, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Task ids:
- `project-store`: Project sync 설정을 Core DB에 저장하는 persistence 경계를 정한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G06.md` → `code_review_local_G06_N.log`, `PLAN-local-G06.md` → `plan_local_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_project_sync_store/`로 이동한다. 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-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Project sync DB schema and SQLC queries | ✅ 완료 |
| [API-2] Storage boundary for active project sync config | ✅ 완료 |
| [API-3] Persistence regression coverage | ✅ 완료 |
## 구현 체크리스트
- [x] `project_sync_settings` migration과 SQLC queries를 추가한다. 검증: Plane project별 하나의 active sync 설정을 찾을 수 있고 inactive 설정은 active lookup에서 제외된다.
- [x] `storage.Store` 또는 좁은 core persistence boundary에 `projectsync.Config` 저장/조회 메서드를 추가한다.
- [x] Project sync 설정 저장/조회 테스트를 작성한다.
- [x] `cd services/core && go test -count=1 ./...`와 `git diff --check`를 실행한다.
- [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_local_G06_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-milestone-work-item-creation-sync/01_project_sync_store/`를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_project_sync_store/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-milestone-work-item-creation-sync/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md`와 `CODE_REVIEW-local-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로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
1. **Upsert query 제거**: SQLC v1.31.1이 CTE + INSERT WHERE NOT EXISTS 패턴을 지원하지 않아 `UpsertProjectSyncSetting` 쿼리를 separate `DeactivateProjectSyncSetting` + `CreateProjectSyncSetting` 호출로 대체했다. callers에서 이 두 쿼리를 순서대로 호출하여 semantic upsert를 구현할 수 있다.
2. **storage → projectsync import 제한**: `storage`가 `projectsync`를 직접 import하면 `storage → projectsync → workitem → workflow → storage` import cycle이 발생한다. 이에 따라 DB ↔ Config 변환 helper를 `projectsync` package(`ToCreateProjectSyncParams`, `ConfigFromDBRecord`)에 두고, storage는 raw DB 파라미터만 사용한다. callers에서 `projectsync.ConfigFromDBRecord`를 호출하여 DB row → Config 변환을 수행한다.
3. **`NewStoreWithQueries` 테스트용 생성자**: 기존 `NewStore`를 유지하면서 테스트용 `NewStoreWithQueries`를 추가하여 mockable하게 했다.
4. **`config.go`에 `db` import 추가**: `ToCreateProjectSyncParams`와 `ConfigFromDBRecord` helper를 위해 `projectsync`가 `db` package를 import하도록 했다. `db`가 `projectsync`를 import하지 않으므로 cycle은 발생하지 않는다.
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Active lookup이 `(provider, tenant, project)` 기준이고 inactive 설정을 반환하지 않는지 확인한다.
- SQLC generated code와 storage 변환이 `projectsync.Config` 필수 필드를 보존하는지 확인한다.
- DB integration test가 없다면 그 공백이 구현 섹션에 명시됐는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### API-1 중간 검증
```
$ cd services/core && go test -count=1 ./internal/db
? github.com/nomadcode/nomadcode-core/internal/db\t[no test files]
```
### API-2 중간 검증
```
$ cd services/core && go test -count=1 ./internal/storage ./internal/projectsync
ok github.com/nomadcode/nomadcode-core/internal/projectsync\t0.003s
ok github.com/nomadcode/nomadcode-core/internal/storage\t0.003s
```
### API-3 중간 검증
```
$ cd services/core && go test -count=1 ./internal/storage ./internal/projectsync
ok github.com/nomadcode/nomadcode-core/internal/projectsync\t0.003s
ok github.com/nomadcode/nomadcode-core/internal/storage\t0.003s
```
### 최종 검증
```
$ cd services/core && go test -count=1 ./...
ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke\t0.003s
? github.com/nomadcode/nomadcode-core/cmd/server\t[no test files]
ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a\t0.006s
ok github.com/nomadcode/nomadcode-core/internal/adapters/jira\t0.008s
ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost\t0.006s
ok github.com/nomadcode/nomadcode-core/internal/adapters/openai\t0.008s
ok github.com/nomadcode/nomadcode-core/internal/adapters/plane\t0.011s
? github.com/nomadcode/nomadcode-core/internal/agent\t[no test files]
ok github.com/nomadcode/nomadcode-core/internal/config\t0.002s
? github.com/nomadcode/nomadcode-core/internal/db\t[no test files]
ok github.com/nomadcode/nomadcode-core/internal/http\t0.004s
? github.com/nomadcode/nomadcode-core/internal/model\t[no test files]
ok github.com/nomadcode/nomadcode-core/internal/notification\t0.004s
ok github.com/nomadcode/nomadcode-core/internal/projectsync\t0.004s
ok github.com/nomadcode/nomadcode-core/internal/protosocket\t0.011s
ok github.com/nomadcode/nomadcode-core/internal/scheduler\t1.510s
ok github.com/nomadcode/nomadcode-core/internal/storage\t0.005s
ok github.com/nomadcode/nomadcode-core/internal/workflow\t0.004s
ok github.com/nomadcode/nomadcode-core/internal/workitem\t0.004s
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline\t0.009s
$ git diff --check
(exit code 0, no whitespace errors)
```
---
> **[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.
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- correctness: Fail
- completeness: Fail
- test coverage: Fail
- API contract: Fail
- code quality: Warn
- plan deviation: Fail
- verification trust: Fail
- 발견된 문제:
- Required: `services/core/internal/storage/project_sync_settings.go:27`에서 `GetActiveProjectSyncSettingByTarget`가 query error를 종류와 무관하게 모두 `ErrProjectSyncNotFound`로 변환합니다. DB 연결 오류, schema 오류, permission 오류까지 "설정 없음"으로 보이므로 caller가 active config 부재와 저장소 장애를 구분할 수 없습니다. `pgx.ErrNoRows`만 not-found로 매핑하고 나머지 오류는 원래 오류를 보존하세요.
- Required: `services/core/internal/storage/project_sync_store_test.go:11` 이하 테스트가 실제 `Store` create/get/deactivate 경계나 active lookup 동작을 호출하지 않고 generated type shape만 확인합니다. 계획의 필수 검증인 "저장/조회, provider/tenant/project 분리, inactive 제외, 필수 필드 reject"가 입증되지 않아 `project-store` persistence regression coverage가 완료되지 않았습니다. fake `db.DBTX` 또는 가능한 DB-backed test로 storage 메서드 호출, target param 전달, `pgx.ErrNoRows`/일반 DB error 처리, active lookup SQL guard를 검증하세요.
- 다음 단계: user-review gate는 트리거하지 않습니다. active plan/review를 로그로 아카이브한 뒤 위 Required 항목만 범위로 하는 후속 `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.md`를 작성합니다.

View file

@ -0,0 +1,204 @@
<!-- task=m-milestone-work-item-creation-sync/01_project_sync_store plan=1 tag=REVIEW_API -->
# 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-06
task=m-milestone-work-item-creation-sync/01_project_sync_store, 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:
- `project-store`: Project sync 설정을 Core DB에 저장하는 persistence 경계를 정한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G07.md` -> `code_review_local_G07_N.log`, `PLAN-local-G07.md` -> `plan_local_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_project_sync_store/`로 이동한다. 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-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
| [REVIEW_API-1] Preserve DB error boundary | [x] |
| [REVIEW_API-2] Meaningful persistence regression coverage | [x] |
- [x] `GetActiveProjectSyncSettingByTarget`가 `pgx.ErrNoRows`만 `ErrProjectSyncNotFound`로 매핑하고 그 외 DB 오류를 보존한다. 검증: fake DBTX/store test가 not found와 일반 DB error를 모두 확인한다.
- [x] Project sync store regression test가 `Store`의 create/get/deactivate 경계를 호출하고 provider/tenant/project param 전달, active lookup SQL guard, inactive 제외 의미를 검증한다.
- [x] `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`, `cd services/core && go test -count=1 ./...`, `git diff --check`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 계획 대비 변경 사항
계획과 동일한 범위대로 구현했다. DB schema 재설계, SQLC query 이름 변경, `projectsync.Config` 필드 의미 변경 등은 모두 수행하지 않았다.
## 주요 설계 결정
1. **fake DBTX mock**: sqlc가 생성한 실제 SQL template 상수 문자열을 mock map의 키로 사용해 정확한 query 매칭을 수행한다. 이는 `db.DBTX` 인터페이스를 구현한 최소한의 mock이다.
2. **mockRow.Scan**: fakeQueryResult.row의 값을 dest pointer에 직접 복사해 실제 `pgx.Row.Scan`의 동작을 시뮬레이션한다.
3. **query text assertion**: mockDBTX가 QueryRow에 전달된 SQL 텍스트를 로그에 기록하고, 테스트에서 `sqlContains`로 `active = TRUE`, `active = FALSE`, `INSERT INTO` 등의 SQL guard가 포함되는지 검증한다.
## 검증 결과
### 중간 검증: `cd services/core && go test -count=1 ./internal/storage`
```
=== RUN TestProjectSyncQueryCompile
--- PASS: TestProjectSyncQueryCompile (0.00s)
=== RUN TestCreateProjectSyncSettingParams
--- PASS: TestCreateProjectSyncSettingParams (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTargetParams
--- PASS: TestGetActiveProjectSyncSettingByTargetParams (0.00s)
=== RUN TestDeactivateProjectSyncSettingParams
--- PASS: TestDeactivateProjectSyncSettingParams (0.00s)
=== RUN TestErrProjectSyncNotFoundIsDefined
--- PASS: TestErrProjectSyncNotFoundIsDefined (0.00s)
=== RUN TestProjectSyncSettingModelShape
--- PASS: TestProjectSyncSettingModelShape (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_ReturnsErrProjectSyncNotFound_OnNoRows
--- PASS: TestGetActiveProjectSyncSettingByTarget_ReturnsErrProjectSyncNotFound_OnNoRows (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_PreservesGenericDBError
--- PASS: TestGetActiveProjectSyncSettingByTarget_PreservesGenericDBError (0.00s)
=== RUN TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs
--- PASS: TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_ReturnsActiveRecord
--- PASS: TestGetActiveProjectSyncSettingByTarget_ReturnsActiveRecord (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_InactiveExcluded
--- PASS: TestGetActiveProjectSyncSettingByTarget_InactiveExcluded (0.00s)
=== RUN TestDeactivateProjectSyncSetting_CallsQueryWithActiveGuard
--- PASS: TestDeactivateProjectSyncSetting_CallsQueryWithActiveGuard (0.00s)
=== RUN TestStoreCalls_PassProviderTenantProjectParams
--- PASS: TestStoreCalls_PassProviderTenantProjectParams (0.00s)
ok github.com/nomadcode/nomadcode-core/internal/storage 0.003s
```
### 최종 검증 1: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`
```
ok github.com/nomadcode/nomadcode-core/internal/storage 0.003s
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.002s
```
### 최종 검증 2: `cd services/core && go test -count=1 ./...`
```
ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.007s
? github.com/nomadcode/nomadcode-core/cmd/server [no test files]
ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.011s
ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.012s
ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.009s
ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.007s
ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 0.008s
? github.com/nomadcode/nomadcode-core/internal/agent [no test files]
ok github.com/nomadcode/nomadcode-core/internal/config 0.002s
? github.com/nomadcode/nomadcode-core/internal/db [no test files]
ok github.com/nomadcode/nomadcode-core/internal/http 0.022s
? github.com/nomadcode/nomadcode-core/internal/model [no test files]
ok github.com/nomadcode/nomadcode-core/internal/notification 0.003s
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.021s
ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.011s
ok github.com/nomadcode/nomadcode-core/internal/scheduler 1.510s
ok github.com/nomadcode/nomadcode-core/internal/storage 0.004s
ok github.com/nomadcode/nomadcode-core/internal/workflow 0.003s
ok github.com/nomadcode/nomadcode-core/internal/workitem 0.003s
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.004s
```
### 최종 검증 3: `git diff --check`
exit code 0. whitespace error 없음.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G07_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-milestone-work-item-creation-sync/01_project_sync_store/`를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_project_sync_store/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-milestone-work-item-creation-sync/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G08.md`와 `CODE_REVIEW-local-G08.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로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `GetActiveProjectSyncSettingByTarget`가 `pgx.ErrNoRows`만 not-found로 바꾸고 일반 DB error를 그대로 반환하는지 확인한다.
- storage tests가 no-op compile test가 아니라 `Store` 메서드를 직접 호출하고 query args와 query text를 assertion하는지 확인한다.
- fake DB test만 사용했다면 실제 DB integration gap이 남는지와, 그 공백이 이번 persistence boundary 완료 판단을 막지 않을 만큼 명시됐는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
---
> **[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.
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- correctness: Pass
- completeness: Fail
- test coverage: Fail
- API contract: Pass
- code quality: Pass
- plan deviation: Fail
- verification trust: Fail
- 발견된 문제:
- Required: `services/core/internal/storage/project_sync_store_test.go:61`의 `mockDBTX.QueryRow`가 `args`를 기록하지 않고, `services/core/internal/storage/project_sync_store_test.go:316`의 `TestStoreCalls_PassProviderTenantProjectParams`도 호출 횟수와 query text만 확인합니다. 따라서 `Store`가 `provider`, `tenant`, `project`를 잘못 전달하거나 순서를 바꿔도 테스트가 통과합니다. G07 계획의 필수 항목인 "provider/tenant/project param 전달" 검증이 아직 충족되지 않았으므로 mock에 args log를 추가하고 create/get/deactivate 각각의 args를 정확히 assertion하세요.
- 다음 단계: user-review gate는 트리거하지 않습니다. active plan/review를 로그로 아카이브한 뒤 위 Required 항목만 범위로 하는 후속 `PLAN-local-G08.md`와 `CODE_REVIEW-local-G08.md`를 작성합니다.

View file

@ -0,0 +1,202 @@
<!-- task=m-milestone-work-item-creation-sync/01_project_sync_store plan=2 tag=REVIEW_REVIEW_API -->
# Code Review Reference - REVIEW_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-06
task=m-milestone-work-item-creation-sync/01_project_sync_store, plan=2, tag=REVIEW_REVIEW_API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Task ids:
- `project-store`: Project sync 설정을 Core DB에 저장하는 persistence 경계를 정한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G08.md` -> `code_review_local_G08_N.log`, `PLAN-local-G08.md` -> `plan_local_G08_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_project_sync_store/`로 이동한다. 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-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REVIEW_API-1] Assert storage query args | [x] |
## 구현 체크리스트
- [x] `mockDBTX.QueryRow`가 SQL text와 `args`를 모두 기록하고, tests가 recorded args를 검증할 수 있게 한다.
- `mockDBTX`에 `queryCalls []queryCall` 필드 추가. `queryCall`은 `sql`과 `args`를 담는 구조체.
- `QueryRow` 구현에서 `m.queryCalls = append(m.queryCalls, queryCall{sql: sql, args: args})`로 args까지 기록.
- [x] `CreateProjectSyncRecord`, `GetActiveProjectSyncSettingByTarget`, `DeactivateProjectSyncSetting` 각각이 expected provider/tenant/project args를 전달하는지 assertion한다. create는 full `CreateProjectSyncSettingParams` field order도 확인한다.
- `assertArgsAt` 헬퍼 함수 추가: `reflect.DeepEqual`로 recorded args와 expected args를 정확히 일치 비교.
- TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs: create args 8개 검증([provider, tenant, project, git_remote_url, source_branch, workspace_id, workspace_base_path, repo_dir_name]).
- `TestGetActiveProjectSyncSettingByTarget_CallsQueryWithCorrectArgs`: get의 3개 args(`[provider, tenant, project]`) 검증.
- `TestDeactivateProjectSyncSetting_CallsQueryWithCorrectArgs`: deactivate의 3개 args(`[provider, tenant, project]`) 검증.
- `TestStoreCalls_PassProviderTenantProjectParams`를 args assertion으로 강화: 3 call 전체의 expected args와 type까지 검증.
- [x] `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`, `cd services/core && go test -count=1 ./...`, `git diff --check`를 실행한다.
- [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_local_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G08_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/01_project_sync_store/`를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_project_sync_store/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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-local-G08.md`와 `CODE_REVIEW-local-G08.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로 이동한다.
## 계획 대비 변경 사항
계획과 동일한 범위대로 구현했다. DB schema, SQLC query, `projectsync.Config` helper는 건드리지 않았고 테스트 helper와 assertions만 수정했다.
## 주요 설계 결정
1. **queryCall 구조체와 queryCalls 필드**: `mockDBTX`에 `queryCalls []queryCall` 필드를 추가해 각 QueryRow 호출 시 SQL text와 args slice를 함께 기록한다. 기존 `queryLog []string`은 그대로 유지해 SQL text assertion 테스트와 호환성을 유지한다.
2. **assertArgsAt 헬퍼 함수**: `reflect.DeepEqual`로 recorded args와 expected args를 인덱스별로 정확히 비교한다. args 개수 불일치와 각각의 value/type 불일치를 상세히 보고한다.
3. **존재하는 테스트 수정**: 기존 `TestStoreCalls_PassProviderTenantProjectParams`를 args assertion으로 완전히 대체했다.
4. **새로운 arg verification 테스트**: `TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs`, `TestGetActiveProjectSyncSettingByTarget_CallsQueryWithCorrectArgs`, `TestDeactivateProjectSyncSetting_CallsQueryWithCorrectArgs`를 새로 추가해 각 메서드별 args 전달을 검증한다.
## 검증 결과
### 중간 검증: `cd services/core && go test -count=1 ./internal/storage`
```
=== RUN TestProjectSyncQueryCompile
--- PASS: TestProjectSyncQueryCompile (0.00s)
=== RUN TestCreateProjectSyncSettingParams
--- PASS: TestCreateProjectSyncSettingParams (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTargetParams
--- PASS: TestGetActiveProjectSyncSettingByTargetParams (0.00s)
=== RUN TestDeactivateProjectSyncSettingParams
--- PASS: TestDeactivateProjectSyncSettingParams (0.00s)
=== RUN TestErrProjectSyncNotFoundIsDefined
--- PASS: TestErrProjectSyncNotFoundIsDefined (0.00s)
=== RUN TestProjectSyncSettingModelShape
--- PASS: TestProjectSyncSettingModelShape (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_ReturnsErrProjectSyncNotFound_OnNoRows
--- PASS: TestGetActiveProjectSyncSettingByTarget_ReturnsErrProjectSyncNotFound_OnNoRows (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_PreservesGenericDBError
--- PASS: TestGetActiveProjectSyncSettingByTarget_PreservesGenericDBError (0.00s)
=== RUN TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs
--- PASS: TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_CallsQueryWithCorrectArgs
--- PASS: TestGetActiveProjectSyncSettingByTarget_CallsQueryWithCorrectArgs (0.00s)
=== RUN TestDeactivateProjectSyncSetting_CallsQueryWithCorrectArgs
--- PASS: TestDeactivateProjectSyncSetting_CallsQueryWithCorrectArgs (0.00s)
=== RUN TestCreateProjectSyncRecord_GetsNotFoundOnMissingRecord
--- PASS: TestCreateProjectSyncRecord_GetsNotFoundOnMissingRecord (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_ReturnsActiveRecord
--- PASS: TestGetActiveProjectSyncSettingByTarget_ReturnsActiveRecord (0.00s)
=== RUN TestGetActiveProjectSyncSettingByTarget_InactiveExcluded
--- PASS: TestGetActiveProjectSyncSettingByTarget_InactiveExcluded (0.00s)
=== RUN TestDeactivateProjectSyncSetting_CallsQueryWithActiveGuard
--- PASS: TestDeactivateProjectSyncSetting_CallsQueryWithActiveGuard (0.00s)
=== RUN TestStoreCalls_PassProviderTenantProjectParams
--- PASS: TestStoreCalls_PassProviderTenantProjectParams (0.00s)
ok github.com/nomadcode/nomadcode-core/internal/storage 0.003s
```
### 최종 검증 1: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`
```
ok github.com/nomadcode/nomadcode-core/internal/storage 0.003s
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.002s
```
### 최종 검증 2: `cd services/core && go test -count=1 ./...`
```
ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.004s
? github.com/nomadcode/nomadcode-core/cmd/server [no test files]
ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.006s
ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.009s
ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.014s
ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.007s
ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 0.011s
? github.com/nomadcode/nomadcode-core/internal/agent [no test files]
ok github.com/nomadcode/nomadcode-core/internal/config 0.002s
? github.com/nomadcode/nomadcode-core/internal/db [no test files]
ok github.com/nomadcode/nomadcode-core/internal/http 0.004s
? github.com/nomadcode/nomadcode-core/internal/model [no test files]
ok github.com/nomadcode/nomadcode-core/internal/notification 0.002s
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.004s
ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.011s
ok github.com/nomadcode/nomadcode-core/internal/scheduler 1.511s
ok github.com/nomadcode/nomadcode-core/internal/storage 0.004s
ok github.com/nomadcode/nomadcode-core/internal/workflow 0.004s
ok github.com/nomadcode/nomadcode-core/internal/workitem 0.004s
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.005s
```
### 최종 검증 3: `git diff --check`
exit code 0. whitespace error 없음.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- mock DB가 QueryRow args를 기록하고 tests가 exact args/order를 assertion하는지 확인한다.
- create/get/deactivate 각각에서 provider/tenant/project 전달을 assertion하는지 확인한다.
- 새 assertion이 wrong args/order regression을 실제로 실패시킬 수 있을 만큼 구체적인지 확인한다.
---
> **[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
- 발견된 문제: 없음
- 다음 단계: active plan/review를 로그로 아카이브하고 `complete.log`를 작성한 뒤, split subtask directory를 `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/01_project_sync_store/`로 이동합니다. PASS completion metadata를 보고하며 roadmap 수정이나 `update-roadmap` 호출은 수행하지 않습니다.

View file

@ -0,0 +1,46 @@
# Complete - m-milestone-work-item-creation-sync/01_project_sync_store
## 완료 일시
2026-06-06
## 요약
Project sync 설정 persistence boundary와 regression coverage를 3회 리뷰 루프 끝에 PASS로 완료했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | DB error boundary가 모든 query error를 not-found로 매핑했고 storage regression coverage가 실제 동작을 검증하지 못했다. |
| `plan_local_G07_1.log` | `code_review_local_G07_1.log` | FAIL | DB error boundary는 해결됐지만 storage mock이 QueryRow args를 기록하지 않아 provider/tenant/project 전달 검증이 부족했다. |
| `plan_local_G08_2.log` | `code_review_local_G08_2.log` | PASS | QueryRow args 기록과 create/get/deactivate exact args assertions가 추가되어 후속 Required 이슈가 닫혔다. |
## 구현/정리 내용
- `project_sync_settings` migration, SQLC query/generated model, storage boundary, `projectsync.Config` DB conversion helper를 추가했다.
- `GetActiveProjectSyncSettingByTarget`가 `pgx.ErrNoRows`만 `ErrProjectSyncNotFound`로 매핑하고 일반 DB error는 보존하도록 정리했다.
- storage tests가 fake DBTX를 통해 query text와 args를 기록하고 create/get/deactivate argument order를 assertion하도록 강화했다.
- 리뷰 중 `services/core/internal/storage/project_sync_store_test.go`를 `gofmt`로 정리했다.
## 최종 검증
- `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync` - PASS; `internal/storage`와 `internal/projectsync` 모두 성공.
- `cd services/core && go test -count=1 ./...` - PASS; core 전체 Go packages 성공.
- `gofmt -l services/core/internal/storage/project_sync_settings.go services/core/internal/storage/project_sync_store_test.go services/core/internal/projectsync/config.go services/core/internal/projectsync/config_test.go` - PASS; 출력 없음.
- `git diff --check` - PASS; whitespace error 없음.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Completed task ids:
- `project-store`: PASS; evidence=`agent-task/archive/2026/06/m-milestone-work-item-creation-sync/01_project_sync_store/plan_local_G08_2.log`, `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/01_project_sync_store/code_review_local_G08_2.log`; verification=`cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`, `cd services/core && go test -count=1 ./...`, `git diff --check`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,147 @@
<!-- task=m-milestone-work-item-creation-sync/01_project_sync_store plan=1 tag=API -->
# Implementation Plan - API
## 이 파일을 읽는 구현 에이전트에게
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증을 실행하고, 실제 변경/출력을 기록하고, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 종료 처리, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret 준비, scope 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.
## 배경
첫 번째 Epic의 순수 모델/경로 계산은 `services/core/internal/projectsync`에 작게 추가됐다. 하지만 Project sync 설정은 아직 DB에 저장되지 않으므로 Plane project별 active 설정 조회가 불가능하다. 이 plan은 `project-store` Task만 완료 대상으로 삼아 persistence 경계를 먼저 만든다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.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:
- `project-store`: Project sync 설정을 Core DB에 저장하는 persistence 경계를 정한다.
- 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/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `.gitignore`
- `services/core/internal/projectsync/config.go`
- `services/core/internal/projectsync/config_test.go`
- `services/core/internal/storage/store.go`
- `services/core/internal/db/db.go`
- `services/core/internal/db/models.go`
- `services/core/queries/tasks.sql`
- `services/core/migrations/00001_create_tasks.sql`
- `services/core/migrations/00002_add_task_external_refs.sql`
- `services/core/migrations/00003_add_task_metadata.sql`
- `services/core/sqlc.yaml`
### 테스트 환경 규칙
- 선택한 `test_env`: `local`.
- `agent-test/local/rules.md`를 읽었다. core 변경은 `agent-test/local/core-smoke.md` 기준으로 최소 `cd services/core && go test ./...`가 필수다.
- workspace-ops는 plan/review artifact 작성에만 닿으므로 `agent-test/local/workspace-ops-smoke.md` 기준 `git diff --check`를 최종 확인에 포함한다.
- `<확인 필요>` 값은 core-smoke의 기준 출력 예시에만 있으며, 이 plan의 명령에는 미해결 환경값이 없다.
### 테스트 커버리지 공백
- Project sync 설정 DB schema/query/store는 신규 동작이라 기존 테스트가 없다. 구현 시 정상 저장/조회, provider project별 active unique, 설정 없음 오류를 테스트로 추가한다.
- SQLC 생성 결과는 unit test와 `go test -count=1 ./...`로 컴파일 검증한다.
### 심볼 참조
- none. 기존 symbol rename/remove는 없다.
### 분할 판단
- split decision policy를 평가했다. 공유 task group은 `agent-task/m-milestone-work-item-creation-sync/`다.
- `01_project_sync_store`: 독립 선행 작업. Project sync 설정 DB 저장소를 만든다.
- `02+01_workspace_slot_store`: `01`의 project sync 설정 identity가 있어야 slot table과 예약이 안정적이다. predecessor `01`: missing active/archive `complete.log`.
- `03+01,02_project_binding_checkout`: 설정 조회와 slot 예약이 모두 있어야 Plane-origin binding과 checkout policy를 연결한다. predecessor `01`, `02`: missing active/archive `complete.log`.
### 범위 결정 근거
- `workspace-slot-state`의 원자 예약, `project-bind`, `workspace-checkout`은 이 plan에서 제외한다. 각각 후속 split plan이 맡는다.
- HTTP endpoint, Plane adapter API, 실제 git checkout 실행은 만들지 않는다. 이 plan은 DB persistence boundary만 다룬다.
- 이미 추가된 `projectsync.Config` 모델은 변경하되 필드 의미가 바뀌지 않게 유지한다.
### 빌드 등급
- `local-G06`: DB migration, SQLC 생성, storage boundary가 포함되지만 범위가 core 내부 persistence로 한정되고 deterministic Go test로 검증 가능하다.
## 구현 체크리스트
- [ ] `project_sync_settings` migration과 SQLC queries를 추가한다. 검증: Plane project별 하나의 active sync 설정을 찾을 수 있고 inactive 설정은 active lookup에서 제외된다.
- [ ] `storage.Store` 또는 좁은 core persistence boundary에 `projectsync.Config` 저장/조회 메서드를 추가한다.
- [ ] Project sync 설정 저장/조회 테스트를 작성한다.
- [ ] `cd services/core && go test -count=1 ./...`와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Project sync DB schema and SQLC queries
- 문제: 현재 migrations는 `tasks` 계열만 정의한다(`services/core/migrations/00003_add_task_metadata.sql:1`). `services/core/queries/tasks.sql:1`도 task query만 있으므로 project sync 설정을 저장하거나 active 설정을 찾을 수 없다.
- 해결 방법: `services/core/migrations/00004_create_project_sync_settings.sql`을 추가한다. 필드는 provider, tenant, project, git_remote_url, source_branch, workspace_id, workspace_base_path, repo_dir_name, active, created_at, updated_at를 포함한다. `(provider, tenant, project) WHERE active` partial unique index를 둔다. `services/core/queries/project_sync_settings.sql`에 create/upsert, get active by target, deactivate queries를 추가하고 `services/core/bin/sqlc` 또는 프로젝트 표준 SQLC 실행으로 generated db code를 갱신한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/migrations/00004_create_project_sync_settings.sql`
- [ ] `services/core/queries/project_sync_settings.sql`
- [ ] `services/core/internal/db/*.go` generated SQLC output
- 테스트 작성: SQL shape는 storage test에서 검증한다. 별도 SQL parser test는 작성하지 않는다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/db`가 컴파일 성공해야 한다. package에 test가 없으면 `?` 출력은 허용한다.
### [API-2] Storage boundary for active project sync config
- 문제: `storage.Store`는 task CRUD만 노출한다(`services/core/internal/storage/store.go:24`). Project sync 설정을 `projectsync.Config`로 저장하거나 provider project target으로 조회하는 API가 없다.
- 해결 방법: `storage`에 project sync input/output adapter를 추가한다. DB row와 `projectsync.Config` 변환은 storage 내부에 둔다. 설정 없음은 호출자가 분기 가능한 package-level error로 반환한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/storage/store.go` 또는 좁은 새 storage file
- [ ] `services/core/internal/projectsync/config.go`는 필드 의미를 바꾸지 않고 필요한 helper만 추가
- 테스트 작성: `services/core/internal/storage/project_sync_store_test.go`를 작성한다. pgx/sqlc boundary를 실제 DB 없이 검증하기 어렵다면 변환 helper unit test와 SQLC compile 검증을 작성하고, 실제 DB integration 부재를 review stub에 남긴다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`.
### [API-3] Persistence regression coverage
- 문제: `agent-roadmap/.../milestone-work-item-creation-sync.md:51`의 검증은 active 설정 lookup이 핵심인데, 기존 테스트는 해당 동작을 다루지 않는다.
- 해결 방법: 정상 저장/조회, provider/tenant/project 분리, inactive 제외, 필수 필드 누락 reject를 테스트한다. DB integration이 없으면 storage 변환과 query compile까지를 이번 pass의 검증 범위로 명시하고 후속 DB integration test gap을 남긴다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/storage/project_sync_store_test.go`
- [ ] 필요한 경우 `services/core/internal/projectsync/config_test.go`
- 테스트 작성: 위 파일에 작성한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/core/migrations/00004_create_project_sync_settings.sql` | API-1 |
| `services/core/queries/project_sync_settings.sql` | API-1 |
| `services/core/internal/db/*.go` | API-1 |
| `services/core/internal/storage/store.go` 또는 `services/core/internal/storage/project_sync_settings.go` | API-2 |
| `services/core/internal/storage/project_sync_store_test.go` | API-3 |
| `services/core/internal/projectsync/config.go` | API-2 |
| `services/core/internal/projectsync/config_test.go` | API-3 |
## 최종 검증
```bash
cd services/core && go test -count=1 ./...
git diff --check
```
기대 결과: 두 명령 모두 exit code 0. Go test cache output은 허용하지 않으므로 `-count=1`을 유지한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,98 @@
<!-- task=m-milestone-work-item-creation-sync/01_project_sync_store plan=1 tag=REVIEW_API -->
# Implementation Plan - REVIEW_API
## 이 파일을 읽는 구현 에이전트에게
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증을 실행하고, 실제 변경/출력을 기록하고, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 종료 처리, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret 준비, scope 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.
## 배경
`project-store` persistence 구현은 컴파일과 전체 Go test는 통과했지만, 첫 리뷰에서 active lookup 오류 경계와 persistence regression coverage가 불충분해 FAIL 판정을 받았다. 이 follow-up은 `project-store` Task 완료를 막는 두 Required 이슈만 수정한다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.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:
- `project-store`: Project sync 설정을 Core DB에 저장하는 persistence 경계를 정한다.
- Completion mode: check-on-pass
## 분석 결과
### 이전 루프
- Archived plan: `agent-task/m-milestone-work-item-creation-sync/01_project_sync_store/plan_local_G06_0.log`
- Archived review: `agent-task/m-milestone-work-item-creation-sync/01_project_sync_store/code_review_local_G06_0.log`
- Verdict: FAIL
### 실패 근거
- `services/core/internal/storage/project_sync_settings.go:27`가 query error를 모두 `ErrProjectSyncNotFound`로 변환해 DB 장애와 설정 부재를 구분할 수 없다.
- `services/core/internal/storage/project_sync_store_test.go:11` 이하 테스트가 실제 storage create/get/deactivate 경계를 호출하지 않아 active lookup, inactive 제외, target param 전달, 일반 DB error 보존을 검증하지 못한다.
### 테스트 환경 규칙
- 선택한 `test_env`: `local`.
- core 변경이므로 최소 `cd services/core && go test -count=1 ./...`를 실행한다.
- task artifact와 문서성 변경이 있으므로 `git diff --check`를 최종 확인에 포함한다.
### 범위 결정 근거
- DB schema 자체를 재설계하지 않는다. partial unique index, SQLC query 이름, `projectsync.Config` 필드 의미는 유지한다.
- 실제 PostgreSQL integration test 환경이 없으면 fake `db.DBTX`/`pgx.Row` 기반 unit test로 generated query 호출, query text, args, scan result, error mapping을 검증한다. DB-backed 검증을 추가하지 못한 사유는 review stub에 남긴다.
- 로드맵 문서나 milestone 상태는 수정하지 않는다. PASS completion event 처리는 runtime 책임이다.
### 빌드 등급
- `local-G07`: 실패 원인은 명시적이고 deterministic하지만, persistence API 오류 경계와 fake DB test evidence를 회복해야 하므로 이전보다 한 단계 높은 local review로 둔다.
## 구현 체크리스트
- [ ] `GetActiveProjectSyncSettingByTarget`가 `pgx.ErrNoRows`만 `ErrProjectSyncNotFound`로 매핑하고 그 외 DB 오류를 보존한다. 검증: fake DBTX/store test가 not found와 일반 DB error를 모두 확인한다.
- [ ] Project sync store regression test가 `Store`의 create/get/deactivate 경계를 호출하고 provider/tenant/project param 전달, active lookup SQL guard, inactive 제외 의미를 검증한다.
- [ ] `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`, `cd services/core && go test -count=1 ./...`, `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_API-1] Preserve DB error boundary
- 문제: active project sync lookup이 모든 query error를 `ErrProjectSyncNotFound`로 반환한다. 이러면 caller가 "active config 없음"과 "DB 장애"를 같은 상태로 처리한다.
- 해결 방법: `services/core/internal/storage/project_sync_settings.go`에서 `errors.Is(err, pgx.ErrNoRows)`일 때만 `ErrProjectSyncNotFound`를 반환하고, 그 외 오류는 원래 오류를 반환한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/storage/project_sync_settings.go`
- [ ] `services/core/internal/storage/project_sync_store_test.go`
- 테스트 작성: fake `db.DBTX`와 fake `pgx.Row`를 사용해 `pgx.ErrNoRows`는 `ErrProjectSyncNotFound`, sentinel DB error는 원래 error로 유지되는지 검증한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/storage`.
### [REVIEW_API-2] Meaningful persistence regression coverage
- 문제: 현재 storage tests는 generated struct/param shape만 확인하고 실제 `Store` 메서드를 호출하지 않는다. 계획의 active lookup, inactive 제외, target 분리 검증이 비어 있다.
- 해결 방법: fake DB boundary를 통해 `CreateProjectSyncRecord`, `GetActiveProjectSyncSettingByTarget`, `DeactivateProjectSyncSetting`이 generated query를 호출하고 provider/tenant/project args를 전달하는지 검증한다. fake DBTX가 받은 query text에 `active = TRUE` lookup guard와 `SET active = FALSE` deactivate guard가 포함되는지도 확인한다. 가능하면 migration file의 partial unique index도 가볍게 assert한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/storage/project_sync_store_test.go`
- [ ] 필요한 경우 test helper를 같은 파일 또는 좁은 `_test.go` 파일에 둔다.
- 테스트 작성: 정상 create/get/deactivate, provider/tenant/project param 전달, inactive 제외 의미, 일반 DB error 보존을 assertion으로 둔다. 단순 "package compiles" no-op test는 제거하거나 실제 assertion으로 대체한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/core/internal/storage/project_sync_settings.go` | REVIEW_API-1 |
| `services/core/internal/storage/project_sync_store_test.go` | REVIEW_API-1, REVIEW_API-2 |
| 필요한 경우 같은 package의 좁은 `_test.go` helper | REVIEW_API-2 |
## 최종 검증
```bash
cd services/core && go test -count=1 ./internal/storage ./internal/projectsync
cd services/core && go test -count=1 ./...
git diff --check
```
기대 결과: 세 명령 모두 exit code 0. Go test cache output은 허용하지 않으므로 `-count=1`을 유지한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,85 @@
<!-- task=m-milestone-work-item-creation-sync/01_project_sync_store plan=2 tag=REVIEW_REVIEW_API -->
# Implementation Plan - REVIEW_REVIEW_API
## 이 파일을 읽는 구현 에이전트에게
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증을 실행하고, 실제 변경/출력을 기록하고, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 종료 처리, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret 준비, scope 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.
## 배경
G07에서 `GetActiveProjectSyncSettingByTarget`의 오류 경계는 `pgx.ErrNoRows`와 일반 DB error를 구분하도록 수정됐다. 하지만 follow-up의 필수 검증이었던 provider/tenant/project parameter 전달 테스트가 아직 실제 args를 assertion하지 않아, wrong args/order regression을 잡지 못한다. 이 plan은 테스트 신뢰도 회복만 다룬다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.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:
- `project-store`: Project sync 설정을 Core DB에 저장하는 persistence 경계를 정한다.
- Completion mode: check-on-pass
## 분석 결과
### 이전 루프
- Archived plan: `agent-task/m-milestone-work-item-creation-sync/01_project_sync_store/plan_local_G07_1.log`
- Archived review: `agent-task/m-milestone-work-item-creation-sync/01_project_sync_store/code_review_local_G07_1.log`
- Verdict: FAIL
### 실패 근거
- `services/core/internal/storage/project_sync_store_test.go:61`의 `mockDBTX.QueryRow`가 SQL text만 기록하고 args를 버린다.
- `services/core/internal/storage/project_sync_store_test.go:316`의 `TestStoreCalls_PassProviderTenantProjectParams`는 호출 횟수와 query text만 확인한다. `Store`가 `provider`, `tenant`, `project`를 잘못 전달해도 테스트가 통과한다.
### 테스트 환경 규칙
- 선택한 `test_env`: `local`.
- core 변경이므로 최소 `cd services/core && go test -count=1 ./...`를 실행한다.
- task artifact와 문서성 변경이 있으므로 `git diff --check`를 최종 확인에 포함한다.
### 범위 결정 근거
- 제품 동작 코드는 이미 G07에서 오류 경계가 수정됐으므로, 새 production behavior 변경은 하지 않는다.
- DB schema, SQLC query, `projectsync.Config` helper는 건드리지 않는다.
- 테스트 helper와 assertions만 고쳐 G07의 남은 required evidence gap을 닫는다.
### 빌드 등급
- `local-G08`: 남은 이슈는 명확한 테스트 assertion 누락이며 deterministic local review로 확인 가능하다.
## 구현 체크리스트
- [ ] `mockDBTX.QueryRow`가 SQL text와 `args`를 모두 기록하고, tests가 recorded args를 검증할 수 있게 한다.
- [ ] `CreateProjectSyncRecord`, `GetActiveProjectSyncSettingByTarget`, `DeactivateProjectSyncSetting` 각각이 expected provider/tenant/project args를 전달하는지 assertion한다. create는 full `CreateProjectSyncSettingParams` field order도 확인한다.
- [ ] `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`, `cd services/core && go test -count=1 ./...`, `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_REVIEW_API-1] Assert storage query args
- 문제: 현재 storage test mock은 `args`를 버리므로 provider/tenant/project 전달 테스트가 이름과 달리 실제 전달값을 검증하지 못한다.
- 해결 방법: `mockDBTX`에 `queryCalls` 또는 동등한 구조를 추가해 SQL text와 args slice를 함께 기록한다. `TestStoreCalls_PassProviderTenantProjectParams` 또는 각 메서드별 test에서 expected args와 recorded args를 `reflect.DeepEqual` 또는 명시 assertion으로 비교한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/storage/project_sync_store_test.go`
- 테스트 작성: create는 `[provider, tenant, project, git_remote_url, source_branch, workspace_id, workspace_base_path, repo_dir_name]`, get/deactivate는 `[provider, tenant, project]`가 정확한 순서로 전달되는지 검증한다. wrong args/order가 들어오면 실패하는 assertion이어야 한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/core/internal/storage/project_sync_store_test.go` | REVIEW_REVIEW_API-1 |
## 최종 검증
```bash
cd services/core && go test -count=1 ./internal/storage ./internal/projectsync
cd services/core && go test -count=1 ./...
git diff --check
```
기대 결과: 세 명령 모두 exit code 0. Go test cache output은 허용하지 않으므로 `-count=1`을 유지한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,142 @@
<!-- task=m-milestone-work-item-creation-sync/02+01_workspace_slot_store plan=1 tag=API -->
# 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`. 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-06
task=m-milestone-work-item-creation-sync/02+01_workspace_slot_store, plan=1, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Task ids:
- `workspace-slot-state`: Workspace slot의 사용중 상태 모델을 정의한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G06.md``code_review_local_G06_N.log`, `PLAN-local-G06.md``plan_local_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/02+01_workspace_slot_store/`로 이동한다. 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-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Workspace slot DB schema and queries | [ ] |
| [API-2] Atomic slot reservation storage API | [ ] |
| [API-3] Slot state regression coverage | [ ] |
## 구현 체크리스트
- [ ] `workspace_slots` migration과 SQLC queries를 추가한다. 검증: 각 slot은 `available`, `in_use`, `dirty`, `error` 상태 후보를 갖는다.
- [ ] `available` slot만 원자적으로 `in_use`로 예약하는 storage method를 추가한다. 검증: 동시에 들어온 요청이 같은 slot을 잡지 않도록 SQL 조건이 있다.
- [ ] Slot state persistence 테스트를 작성한다.
- [ ] `cd services/core && go test -count=1 ./...``git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_local_G06_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G06_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-milestone-work-item-creation-sync/02+01_workspace_slot_store/``agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/02+01_workspace_slot_store/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-milestone-work-item-creation-sync/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md``CODE_REVIEW-local-G06.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로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Slot state check constraint와 Go enum이 같은 후보만 허용하는지 확인한다.
- 예약 query가 `available` 조건을 한 statement에서 보장하는지 확인한다.
- predecessor `01_project_sync_store`의 완료 근거가 있는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### API-1 중간 검증
```
$ cd services/core && go test -count=1 ./internal/db
(output)
```
### API-2 중간 검증
```
$ cd services/core && go test -count=1 ./internal/storage ./internal/projectsync
(output)
```
### API-3 중간 검증
```
$ cd services/core && go test -count=1 ./internal/storage ./internal/projectsync
(output)
```
### 최종 검증
```
$ cd services/core && go test -count=1 ./...
(output)
$ git diff --check
(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.

View file

@ -0,0 +1,149 @@
<!-- task=m-milestone-work-item-creation-sync/02+01_workspace_slot_store plan=1 tag=API -->
# Implementation Plan - API
## 이 파일을 읽는 구현 에이전트에게
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증을 실행하고, 실제 변경/출력을 기록하고, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 종료 처리, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret 준비, scope 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.
## 배경
`projectsync`에는 `available`, `in_use`, `dirty`, `error` 상태와 3자리 slot index 모델이 있다. 하지만 Core DB에는 slot 상태가 저장되지 않고, 동시에 들어온 요청이 같은 slot을 잡지 않도록 원자적으로 `in_use`로 바꾸는 persistence 경계가 없다. 이 plan은 `workspace-slot-state` Task의 DB 저장과 예약 기준만 완료 대상으로 한다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.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:
- `workspace-slot-state`: Workspace slot의 사용중 상태 모델을 정의한다.
- 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/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `.gitignore`
- `services/core/internal/projectsync/config.go`
- `services/core/internal/projectsync/config_test.go`
- `services/core/internal/storage/store.go`
- `services/core/queries/tasks.sql`
- `services/core/migrations/00003_add_task_metadata.sql`
- `services/core/sqlc.yaml`
### 테스트 환경 규칙
- 선택한 `test_env`: `local`.
- `agent-test/local/rules.md`를 읽었다. core 변경은 `agent-test/local/core-smoke.md` 기준으로 최소 `cd services/core && go test ./...`가 필수다.
- workspace-ops는 plan/review artifact 작성에만 닿으므로 `agent-test/local/workspace-ops-smoke.md` 기준 `git diff --check`를 최종 확인에 포함한다.
- `<확인 필요>` 값은 core-smoke의 기준 출력 예시에만 있으며, 이 plan의 명령에는 미해결 환경값이 없다.
### 테스트 커버리지 공백
- Slot state enum과 path/index helper는 `services/core/internal/projectsync/config_test.go`에서 일부 검증된다.
- DB 원자 예약은 신규 동작이라 기존 테스트가 없다. 구현 시 `available` slot만 예약되고, 이미 `in_use`인 slot은 예약되지 않는 경계를 테스트한다.
- 실제 동시 transaction race는 local unit test에서 DB 없이 완전 검증하기 어렵다. SQL `UPDATE ... WHERE state='available' RETURNING` 형태와 storage-level behavior를 검증하고 남은 DB integration 공백을 기록한다.
### 심볼 참조
- none. 기존 symbol rename/remove는 없다.
### 분할 판단
- split decision policy를 평가했다. 공유 task group은 `agent-task/m-milestone-work-item-creation-sync/`다.
- `01_project_sync_store`: predecessor. Project sync 설정 DB 저장소를 만든다.
- `02+01_workspace_slot_store`: 현재 plan. predecessor `01`: missing active/archive `complete.log`. 구현은 `01_project_sync_store` PASS 후 시작한다.
- `03+01,02_project_binding_checkout`: 후속. 설정 조회와 slot 예약이 모두 있어야 binding/checkout을 연결한다.
### 범위 결정 근거
- Project sync 설정 저장소 생성은 predecessor `01` 범위다. 이 plan은 해당 설정 row를 참조하는 workspace slot 상태 저장과 원자 예약만 다룬다.
- 실제 git checkout, workspace 디렉터리 생성, Plane-origin authoring 실행은 제외한다.
- slot 상태 후보는 `projectsync.SlotState`를 source of truth로 유지하고 중복 enum을 만들지 않는다.
### 빌드 등급
- `local-G06`: DB update 조건과 상태 전이가 핵심이지만 범위가 storage 내부로 한정되고 focused tests plus full Go test로 검증 가능하다.
## 구현 체크리스트
- [ ] `workspace_slots` migration과 SQLC queries를 추가한다. 검증: 각 slot은 `available`, `in_use`, `dirty`, `error` 상태 후보를 갖는다.
- [ ] `available` slot만 원자적으로 `in_use`로 예약하는 storage method를 추가한다. 검증: 동시에 들어온 요청이 같은 slot을 잡지 않도록 SQL 조건이 있다.
- [ ] Slot state persistence 테스트를 작성한다.
- [ ] `cd services/core && go test -count=1 ./...``git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- 이 subtask 디렉터리 이름은 `02+01_workspace_slot_store`이므로 predecessor는 `01_project_sync_store` 하나다.
- 구현 시작 전 같은 task group의 `01_project_sync_store/complete.log`가 active 또는 archive 경로에 있어야 한다. 현재 확인 시 predecessor completion은 missing이다.
### [API-1] Workspace slot DB schema and queries
- 문제: `projectsync.SlotState`는 코드 모델로 존재하지만(`services/core/internal/projectsync/config.go:36`), DB에는 slot 상태를 저장할 table이 없다. milestone은 상태 후보와 DB 원자 전환 기준을 요구한다(`agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:55`).
- 해결 방법: `services/core/migrations/00005_create_workspace_slots.sql`을 추가한다. `project_sync_setting_id`, `slot_index`, `state`, `path`, timestamps를 포함하고 `(project_sync_setting_id, slot_index)` unique를 둔다. `state` check constraint는 `available`, `in_use`, `dirty`, `error`만 허용한다. SQLC queries에는 list by project, upsert, reserve next available, update state를 둔다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/migrations/00005_create_workspace_slots.sql`
- [ ] `services/core/queries/workspace_slots.sql`
- [ ] `services/core/internal/db/*.go` generated SQLC output
- 테스트 작성: storage-level tests에서 query behavior를 검증한다. SQL parser-only test는 작성하지 않는다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/db`.
### [API-2] Atomic slot reservation storage API
- 문제: `storage.Store`는 task CRUD만 제공한다(`services/core/internal/storage/store.go:24`). 새 작업이 `available` slot만 선택하고 같은 slot을 동시에 잡지 않도록 하는 API가 없다.
- 해결 방법: storage에 `ReserveWorkspaceSlot(ctx, projectSyncSettingID string)`와 상태 update method를 추가한다. SQL은 한 statement에서 `state='available'` 조건과 `ORDER BY slot_index LIMIT 1 FOR UPDATE SKIP LOCKED` 또는 동등한 원자 update pattern을 사용한다. 반환 타입은 `projectsync.SlotIndex`, `projectsync.SlotState`, path를 보존한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/storage/store.go` 또는 `workspace_slots.go`
- [ ] `services/core/internal/projectsync/config.go` helper가 필요하면 최소 수정
- 테스트 작성: `services/core/internal/storage/workspace_slots_test.go`에 available 예약, in_use 제외, dirty/error 제외, no available error를 작성한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`.
### [API-3] Slot state regression coverage
- 문제: 모델 test는 `SlotStateAvailable.Allocatable()`만 확인한다(`services/core/internal/projectsync/config_test.go`). Persistence의 상태 전이는 아직 검증되지 않는다.
- 해결 방법: storage test에서 상태 후보와 예약 전후 state change를 검증한다. 실제 DB가 필요한 부분은 테스트 fixture를 명시하고, DB 없는 환경이면 query compile과 converter test로 대체한 사유를 review stub에 기록한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/storage/workspace_slots_test.go`
- [ ] 필요한 경우 `services/core/internal/projectsync/config_test.go`
- 테스트 작성: 위 파일에 작성한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/storage ./internal/projectsync`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/core/migrations/00005_create_workspace_slots.sql` | API-1 |
| `services/core/queries/workspace_slots.sql` | API-1 |
| `services/core/internal/db/*.go` | API-1 |
| `services/core/internal/storage/workspace_slots.go` 또는 `services/core/internal/storage/store.go` | API-2 |
| `services/core/internal/storage/workspace_slots_test.go` | API-3 |
| `services/core/internal/projectsync/config.go` | API-2 |
| `services/core/internal/projectsync/config_test.go` | API-3 |
## 최종 검증
```bash
cd services/core && go test -count=1 ./...
git diff --check
```
기대 결과: 두 명령 모두 exit code 0. Go test cache output은 허용하지 않으므로 `-count=1`을 유지한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,144 @@
<!-- task=m-milestone-work-item-creation-sync/03+01,02_project_binding_checkout plan=1 tag=API -->
# 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`. 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-06
task=m-milestone-work-item-creation-sync/03+01,02_project_binding_checkout, plan=1, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Task ids:
- `project-bind`: Plane-origin 생성 동기화가 project sync 설정을 통해 Plane project, git repository, 작업 workspace를 해석하도록 한다.
- `workspace-checkout`: Git repository checkout 정책을 project sync 설정과 연결한다.
- 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/03+01,02_project_binding_checkout/`로 이동한다. 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-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Project sync resolver binding | [ ] |
| [API-2] Checkout policy object from config and slot | [ ] |
| [API-3] Handler/server wiring for project sync dependencies | [ ] |
## 구현 체크리스트
- [ ] Plane-origin 생성 동기화가 project sync 설정 resolver를 통해 Plane project, git repository, workspace를 해석하도록 한다. 검증: 같은 Plane project 안의 티켓은 같은 git/workspace 설정을 사용하며, 다른 Plane project와 섞이지 않는다.
- [ ] Git checkout policy를 project sync 설정과 workspace slot에 연결한다. 검증: git remote는 project sync 설정에서 읽고, 각 slot은 같은 remote의 독립 checkout path를 기본으로 확보하며, 일반 단일 운용에서는 `000`을 사용한다.
- [ ] Binding/checkout policy 테스트를 작성한다.
- [ ] `cd services/core && go test -count=1 ./...``git diff --check`를 실행한다.
- [ ] 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이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-milestone-work-item-creation-sync/03+01,02_project_binding_checkout/``agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/03+01,02_project_binding_checkout/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] 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로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Binding이 request body workspace/git 값을 신뢰하지 않고 project sync config를 통해 해석하는지 확인한다.
- 같은 Plane project와 다른 Plane project의 config/slot 분리가 테스트됐는지 확인한다.
- Checkout policy가 실제 git process 실행까지 몰래 확장되지 않았는지 확인한다.
- predecessor `01_project_sync_store`, `02+01_workspace_slot_store` 완료 근거가 있는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### API-1 중간 검증
```
$ cd services/core && go test -count=1 ./internal/workitempipeline ./internal/projectsync
(output)
```
### API-2 중간 검증
```
$ cd services/core && go test -count=1 ./internal/projectsync
(output)
```
### API-3 중간 검증
```
$ cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline
(output)
```
### 최종 검증
```
$ cd services/core && go test -count=1 ./...
(output)
$ git diff --check
(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.

View file

@ -0,0 +1,152 @@
<!-- task=m-milestone-work-item-creation-sync/03+01,02_project_binding_checkout plan=1 tag=API -->
# Implementation Plan - API
## 이 파일을 읽는 구현 에이전트에게
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증을 실행하고, 실제 변경/출력을 기록하고, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 종료 처리, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret 준비, scope 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`를 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.
## 배경
Project sync 설정과 workspace slot 저장소가 준비되면 Plane-origin 요청은 더 이상 요청 body만으로 git/workspace를 해석하면 안 된다. 같은 Plane project의 티켓은 같은 git/workspace 설정을 사용해야 하고, 실제 작업 slot은 독립 checkout 경로로 계획되어야 한다. 이 plan은 `project-bind``workspace-checkout` Task를 함께 닫는 후속 binding 작업이다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.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:
- `project-bind`: Plane-origin 생성 동기화가 project sync 설정을 통해 Plane project, git repository, 작업 workspace를 해석하도록 한다.
- `workspace-checkout`: Git repository checkout 정책을 project sync 설정과 연결한다.
- 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/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `.gitignore`
- `services/core/internal/projectsync/config.go`
- `services/core/internal/projectsync/config_test.go`
- `services/core/internal/workitempipeline/service.go`
- `services/core/internal/workitempipeline/service_test.go`
- `services/core/internal/http/handlers.go`
- `services/core/cmd/server/main.go`
- `services/core/internal/storage/store.go`
### 테스트 환경 규칙
- 선택한 `test_env`: `local`.
- `agent-test/local/rules.md`를 읽었다. core 변경은 `agent-test/local/core-smoke.md` 기준으로 최소 `cd services/core && go test ./...`가 필수다.
- workspace-ops는 plan/review artifact 작성에만 닿으므로 `agent-test/local/workspace-ops-smoke.md` 기준 `git diff --check`를 최종 확인에 포함한다.
- 이 plan은 checkout process 실행 자체를 만들지 않도록 제한한다. process control을 구현하게 되면 `cloud-G07` 판단을 유지하고 검증 명령에 focused process tests를 추가해야 한다.
### 테스트 커버리지 공백
- 현재 `workitempipeline.Service`는 provider reader와 task creator만 가진다(`services/core/internal/workitempipeline/service.go:21`). Project config resolver, slot reserver, checkout plan을 다루는 테스트가 없다.
- HTTP handler는 provider registry만 조립한다(`services/core/internal/http/handlers.go:39`). Project sync resolver wiring 테스트가 없다.
- 실제 git checkout 실행은 이 plan 범위가 아니다. checkout policy는 계획 객체와 path/remote/branch 검증으로 테스트한다.
### 심볼 참조
- none. 기존 symbol rename/remove는 없다. `workitempipeline.New` signature를 바꾸는 경우 call site는 `services/core/internal/http/handlers.go:51`, `services/core/internal/workitempipeline/service_test.go:41`, `services/core/cmd/server/main.go:109` 경유 handler wiring을 모두 갱신한다.
### 분할 판단
- split decision policy를 평가했다. 공유 task group은 `agent-task/m-milestone-work-item-creation-sync/`다.
- `01_project_sync_store`: predecessor. Project sync 설정 DB 저장소를 만든다.
- `02+01_workspace_slot_store`: predecessor. Slot 상태 저장과 원자 예약을 만든다.
- `03+01,02_project_binding_checkout`: 현재 plan. predecessor `01`, `02`: missing active/archive `complete.log`. 구현은 두 predecessor PASS 후 시작한다.
### 범위 결정 근거
- Plane ticket authoring run 실행, IOP CLI 호출, Plane Todo projection은 첫 Epic 밖의 후속 Epic/Task다.
- 실제 `git clone`, `git fetch`, `git checkout` 프로세스 실행은 하지 않는다. 이 plan은 config와 reserved slot에서 checkout plan을 산출하는 정책 경계까지만 둔다.
- UI 구현은 `settings-ui` Task의 후속 후보로 남기고 이 plan에서 제외한다.
### 빌드 등급
- `cloud-G07`: storage 결과를 pipeline/handler wiring에 연결하고 checkout policy를 세우는 cross-boundary 작업이다. 실제 terminal checkout 실행은 제외하지만, 선행 plan 산출물과 여러 call site 맥락을 안전하게 붙여야 한다.
## 구현 체크리스트
- [ ] Plane-origin 생성 동기화가 project sync 설정 resolver를 통해 Plane project, git repository, workspace를 해석하도록 한다. 검증: 같은 Plane project 안의 티켓은 같은 git/workspace 설정을 사용하며, 다른 Plane project와 섞이지 않는다.
- [ ] Git checkout policy를 project sync 설정과 workspace slot에 연결한다. 검증: git remote는 project sync 설정에서 읽고, 각 slot은 같은 remote의 독립 checkout path를 기본으로 확보하며, 일반 단일 운용에서는 `000`을 사용한다.
- [ ] Binding/checkout policy 테스트를 작성한다.
- [ ] `cd services/core && go test -count=1 ./...``git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- 이 subtask 디렉터리 이름은 `03+01,02_project_binding_checkout`이므로 predecessor는 `01_project_sync_store`, `02+01_workspace_slot_store` 두 개다.
- 구현 시작 전 같은 task group의 `01_project_sync_store/complete.log``02+01_workspace_slot_store/complete.log`가 active 또는 archive 경로에 있어야 한다. 현재 확인 시 두 predecessor completion은 missing이다.
### [API-1] Project sync resolver binding
- 문제: `workitempipeline.Service`는 provider work item을 task로 바꾸지만 project sync 설정을 읽지 않는다(`services/core/internal/workitempipeline/service.go:30`). milestone은 Plane project별 sync 설정으로 Plane project, git repository, 작업 workspace를 해석하라고 요구한다(`agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:52`).
- 해결 방법: `workitempipeline`에 좁은 interface를 추가한다. 예: `ProjectConfigResolver.ResolveProjectSyncConfig(ctx, workitem.Ref) (projectsync.Config, error)`. Service input 처리 중 normalized ref로 resolver를 호출하고, 설정 없음이면 task creation으로 넘어가지 않는다. 같은 provider/tenant/project는 같은 config를 반환하고 다른 project는 분리된 config를 쓰도록 테스트한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/workitempipeline/service.go`
- [ ] `services/core/internal/workitempipeline/service_test.go`
- [ ] 필요한 경우 `services/core/internal/http/handlers.go`
- 테스트 작성: fake resolver로 same project/different project behavior, resolver error propagation, no config blocks task creation을 테스트한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/workitempipeline ./internal/projectsync`.
### [API-2] Checkout policy object from config and slot
- 문제: `projectsync.Config`는 git remote, source branch, workspace path를 갖지만(`services/core/internal/projectsync/config.go:25`), slot별 독립 checkout path와 연결된 policy 객체가 없다. milestone은 git remote를 project sync 설정에서 읽고 slot별 독립 checkout을 기본으로 하라고 요구한다(`agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:56`).
- 해결 방법: `projectsync.CheckoutPlan` 또는 동등한 타입을 추가한다. 입력은 normalized config와 reserved slot이다. 출력은 remote URL, source branch, project workspace root, slot path, slot index를 담는다. 실제 git process는 실행하지 않는다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/projectsync/config.go` 또는 `checkout.go`
- [ ] `services/core/internal/projectsync/config_test.go` 또는 `checkout_test.go`
- 테스트 작성: `000` 기본 slot, `001` 병렬 slot, remote/branch passthrough, invalid config/slot reject를 테스트한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/projectsync`.
### [API-3] Handler/server wiring for project sync dependencies
- 문제: HTTP handler는 provider reader만 받아 pipeline service를 만든다(`services/core/internal/http/handlers.go:39`). Server wiring도 provider만 넘긴다(`services/core/cmd/server/main.go:109`). Resolver와 slot reserver가 wiring되지 않으면 runtime path에서 binding이 적용되지 않는다.
- 해결 방법: predecessor에서 추가한 storage methods를 handler 또는 pipeline options로 주입한다. nil dependency는 명시적으로 sync 설정 없음/서비스 미구성 오류로 실패하게 하고, 기존 provider-neutral task creation test를 갱신한다.
- 수정 파일 및 체크리스트:
- [ ] `services/core/internal/http/handlers.go`
- [ ] `services/core/internal/http/handlers_test.go`
- [ ] `services/core/cmd/server/main.go`
- 테스트 작성: handler test에 configured resolver path, missing project sync 설정 path, provider mismatch 유지 test를 추가한다.
- 중간 검증: `cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/core/internal/workitempipeline/service.go` | API-1 |
| `services/core/internal/workitempipeline/service_test.go` | API-1 |
| `services/core/internal/projectsync/config.go` 또는 `services/core/internal/projectsync/checkout.go` | API-2 |
| `services/core/internal/projectsync/config_test.go` 또는 `services/core/internal/projectsync/checkout_test.go` | API-2 |
| `services/core/internal/http/handlers.go` | API-3 |
| `services/core/internal/http/handlers_test.go` | API-3 |
| `services/core/cmd/server/main.go` | API-3 |
## 최종 검증
```bash
cd services/core && go test -count=1 ./...
git diff --check
```
기대 결과: 두 명령 모두 exit code 0. Go test cache output은 허용하지 않으므로 `-count=1`을 유지한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -308,12 +308,116 @@
--- ---
## 5. Workspace / Project Metadata 계약 후보 ## 5. Roadmap / Work Item Sync 계약 후보
> [!IMPORTANT] > [!IMPORTANT]
> **Workspace/Project Metadata 상태**: 현재 Go core 백엔드에 이와 관련한 별도의 Source-of-Truth 저장소나 전용 테이블 구조가 존재하지 않으며, API 명세 또한 완전히 확정되지 않았습니다. 아래는 Flutter 앱과 코어 간 향후 연동을 대비한 **계약 후보 필드 목록**입니다. > 이 절은 source schema가 아니라 Plane/Jira 같은 provider work item과 `agent-roadmap` Milestone item을 같은 구조로 유지하기 위한 sync domain 계약 후보입니다. 실제 구현 전까지는 compatibility note로만 사용합니다.
### 5.1 Workspace Metadata 후보 스펙 ### 5.1 Sync Domain 책임 후보
- **설명**: Core 안의 sync domain은 provider adapter 위에서 동작하며, provider native API 세부가 아니라 Milestone/work item identity, revision, 변경 감지, 수렴 적용, conflict review gate를 소유한다. Milestone sync의 source of truth는 `develop` branch의 `agent-roadmap`이다.
- **적용 대상**:
- Plane/Jira 상위 work item ↔ `develop` branch의 `agent-roadmap` Milestone
- Plane/Jira 하위 work item ↔ `develop` branch의 Milestone `기능` Task item
- `agent-task/m-<milestone-slug>` plan/code-review/completion event ↔ Milestone Task ↔ provider child work item
- **비소유 대상**:
- Plane/Jira native DTO와 endpoint 세부
- 사용자 승인 없는 archive/delete 강제 적용
- provider별 custom field 전제 설계
### 5.2 Sync Identity 후보
- **설명**: 어느 쪽에서 변경이 발생해도 같은 Milestone item 구조로 찾고 비교하기 위한 provider-neutral identity shape다.
- **후보 필드**:
| 필드 | 설명 |
|------|------|
| `shape` | `milestone` 또는 `task` |
| `roadmap_milestone_path` | `agent-roadmap/phase/<phase-slug>/milestones/<milestone-slug>.md` |
| `roadmap_item_id` | Milestone 자체이면 파일 slug, Task이면 Milestone 안의 item-id |
| `provider` | `plane`, `jira` 등 provider id |
| `tenant` | provider workspace/site/account 식별자 |
| `project` | provider project 식별자 |
| `work_item_id` | provider work item id 또는 issue key |
| `parent_work_item_id` | Task shape일 때 provider 상위 work item id |
| `roadmap_revision` | roadmap 문서 또는 Core index가 본 revision |
| `roadmap_branch` | source of truth branch. Milestone sync에서는 기본값 `develop` |
| `provider_revision` | provider가 제공하는 updated timestamp/version/hash 후보 |
| `observed_at` | Core가 변경을 관측한 시각 |
### 5.3 Project Sync Configuration 후보
- **설명**: Milestone sync는 Plane/Jira provider project 단위로 묶이며, Core DB에 저장된 project sync 설정을 통해 provider project, git repository, 실제 작업 workspace를 해석한다.
- **저장 위치**: Core DB. source schema와 migration은 구현 시점에 확정한다.
- **후보 필드**:
| 필드 | 설명 |
|------|------|
| `sync_project_id` | Core 내부 project sync 설정 id |
| `provider` | `plane`, `jira` 등 provider id |
| `provider_tenant` | Plane workspace slug 또는 Jira site/account 식별자 |
| `provider_project_id` | Plane project id 또는 Jira project id/key |
| `provider_project_name` | 표시용 provider project 이름 |
| `git_remote_url` | source-of-truth roadmap이 있는 git repository URL |
| `git_default_branch` | source-of-truth branch. 기본값 `develop` |
| `roadmap_root_path` | repository 안의 roadmap root. 기본값 `agent-roadmap` |
| `workspace_id` | 실제 작업 workspace 식별자 |
| `workspace_path` | local/runner에서 실제 작업이 돌아갈 workspace path 후보 |
| `enabled` | sync 활성화 여부 |
| `settings` | provider별 확장 설정 JSON |
| `created_at` / `updated_at` | 생성/수정 시각 |
- **운영 원칙**:
- Plane/Jira work item intake와 projection은 반드시 project sync 설정을 통해 대상 git/workspace를 확정한 뒤 실행한다.
- 같은 provider project의 Milestone/work item sync는 같은 active project sync 설정을 사용한다.
- project sync 설정이 없거나 둘 이상이면 Milestone 생성 동기화를 진행하지 않고 사용자 설정 또는 운영자 조치가 필요하다고 보고한다.
- 이 설정은 이후 Project settings UI/UX에서 확인/수정할 수 있어야 한다.
### 5.4 Sync Change Event 후보
- **설명**: provider webhook, provider polling, roadmap file/index scan, agent-task completion event를 같은 변경 입력으로 정규화한다.
- **proto-socket event 후보**:
| action | type | channel | payload |
|--------|------|---------|---------|
| `roadmap_sync.changed` | `event` | `roadmap_sync` | `{ "source", "change_type", "identity", "fields", "actor?", "reason?", "observed_at" }` |
- **payload 후보 필드**:
- `source`: `provider_webhook`, `provider_poll`, `roadmap_scan`, `agent_task_completion`, `manual`
- `change_type`: `created`, `updated`, `status_changed`, `parent_changed`, `completed`, `archived`, `deleted`
- `identity`: 5.2의 Sync Identity shape
- `fields`: title, description, status, labels, parent-child, order, review state 같은 변경 후보
- `actor`: provider user, NomadCode actor, external agent 등 변경 주체
- `reason`: 사용자 요청, completion event, scheduled reconciliation 등 적용 사유
### 5.5 Sync Action 후보
- **설명**: 외부 agent, Flutter UI, scheduler가 sync domain에 검사와 적용을 요청할 때 사용하는 action 후보다.
| action | request payload | response payload |
|--------|-----------------|------------------|
| `roadmap_sync.inspect` | `{ "identity?", "scope?", "source?", "dry_run": true }` | `{ "matches", "drift", "conflicts" }` |
| `roadmap_sync.apply` | `{ "identity", "change", "expected_revision?", "idempotency_key", "actor", "reason", "dry_run": false }` | `{ "applied", "identity", "roadmap_revision", "provider_revision", "conflict?" }` |
| `roadmap_sync.reconcile` | `{ "scope", "provider?", "since?", "dry_run": true }` | `{ "changes", "proposals", "conflicts" }` |
- **공통 입력 원칙**:
- destructive 변경, archive, delete, 사용자 승인 상태 변경은 `dry_run` 또는 review gate를 우선한다.
- `expected_revision`이 맞지 않으면 조용히 덮어쓰지 않고 conflict로 반환한다.
- 같은 변경 재시도는 `idempotency_key`로 중복 적용을 피한다.
- provider webhook이 없거나 누락될 수 있으므로 scheduler polling과 같은 action shape를 공유한다.
- feature branch 또는 로컬 초안은 Plane/Jira `Todo` projection 대상이 아니며, `develop` branch에 반영된 roadmap만 sync 확정본으로 적용한다.
### 5.6 Sync Convergence 정책 후보
- `develop` branch의 Milestone과 provider parent work item은 같은 목표, 범위 요약, 상태, review gate를 표현한다.
- Milestone `기능` Task와 provider child work item은 같은 제목/상태/parent 관계를 표현한다.
- roadmap 쪽 변경과 provider 쪽 변경은 모두 sync change event로 정규화한 뒤 Core action을 통해 적용한다.
- 동시에 수정된 항목은 revision mismatch로 멈추고, 사용자 검토 또는 dry-run proposal로 올린다.
- provider 상태는 가볍게 유지하고, agent 내부 실행 상태는 metadata/labels/comment로 투영한다.
- `Done`/archive/delete처럼 되돌리기 어려운 변경은 사용자 승인 또는 명시 action 없이 자동 적용하지 않는다.
---
## 6. Workspace / Project Metadata 계약 후보
> [!IMPORTANT]
> **Workspace/Project Metadata 상태**: 일반 Workspace/Project metadata는 아직 완전한 source schema가 없으며 아래는 후보 필드 목록입니다. 단, Milestone sync에 필요한 provider project, git remote, workspace 설정은 5.3의 Project Sync Configuration 후보로 분리하고 Core DB 저장 대상으로 본다.
### 6.1 Workspace Metadata 후보 스펙
- **설명**: 에이전트 작업 공간(Workspace)에 대한 설정 및 상태 메타데이터. - **설명**: 에이전트 작업 공간(Workspace)에 대한 설정 및 상태 메타데이터.
- **후보 필드**: - **후보 필드**:
- `workspace_id`: 작업 공간 고유 식별자 (UUID) - `workspace_id`: 작업 공간 고유 식별자 (UUID)
@ -325,7 +429,7 @@
- `status`: 현재 상태 (예: `active`, `archived`, `suspended`) - `status`: 현재 상태 (예: `active`, `archived`, `suspended`)
- `settings`: 작업 공간에 특화된 동적 설정 JSON 오브젝트 (예: 자동 스케줄링 옵션, 알림 채널 정보 등) - `settings`: 작업 공간에 특화된 동적 설정 JSON 오브젝트 (예: 자동 스케줄링 옵션, 알림 채널 정보 등)
### 5.2 Project Metadata 후보 스펙 ### 6.2 Project Metadata 후보 스펙
- **설명**: 작업 공간 내 세부 프로젝트(Project) 정보. - **설명**: 작업 공간 내 세부 프로젝트(Project) 정보.
- **후보 필드**: - **후보 필드**:
- `project_id`: 프로젝트 고유 식별자 (UUID) - `project_id`: 프로젝트 고유 식별자 (UUID)
@ -338,13 +442,13 @@
--- ---
## 6. 클라이언트 Integration 설정 계약 후보 ## 7. 클라이언트 Integration 설정 계약 후보
> [!IMPORTANT] > [!IMPORTANT]
> 이 절은 `apps/client`의 integration boundary에서 host/plugin/transport 사이에 교환되는 설정값을 후보로 모은 것이다. 실제 source schema는 소비하는 core/client 경계가 안정화된 뒤로 미룬다. > 이 절은 `apps/client`의 integration boundary에서 host/plugin/transport 사이에 교환되는 설정값을 후보로 모은 것이다. 실제 source schema는 소비하는 core/client 경계가 안정화된 뒤로 미룬다.
> 관련 코드 경계: `apps/client/lib/src/integrations/proto_socket/``apps/client/lib/src/integrations/mattermost/`. > 관련 코드 경계: `apps/client/lib/src/integrations/proto_socket/``apps/client/lib/src/integrations/mattermost/`.
### 6.1 proto-socket Endpoint 설정 후보 ### 7.1 proto-socket Endpoint 설정 후보
- **설명**: `apps/client`가 proto-socket transport에 의존할 때 사용하는 연결/하트비트 설정. 현재 구현은 `proto_socket` Dart 패키지(local path)를 끌어 쓰며 bootstrap에서 자동 연결하지 않는다. - **설명**: `apps/client`가 proto-socket transport에 의존할 때 사용하는 연결/하트비트 설정. 현재 구현은 `proto_socket` Dart 패키지(local path)를 끌어 쓰며 bootstrap에서 자동 연결하지 않는다.
- **후보 필드**: - **후보 필드**:
- `host`: 대상 서버 hostname - `host`: 대상 서버 hostname
@ -355,7 +459,7 @@
- `heartbeat_wait_seconds`: heartbeat 응답 대기 한계 (기본 60) - `heartbeat_wait_seconds`: heartbeat 응답 대기 한계 (기본 60)
- `enabled`: bootstrap 시 자동 연결 활성화 여부 (기본 false) - `enabled`: bootstrap 시 자동 연결 활성화 여부 (기본 false)
### 6.2 Mattermost Push Host 책임 경계 후보 ### 7.2 Mattermost Push Host 책임 경계 후보
- **설명**: Mattermost push 통합에서 `apps/client` host가 plugin adapter에게 위임/공급해야 하는 데이터와 콜백. plugin adapter는 platform 측 plugin singleton에 격리되어 있고, 그 외 코드는 모두 `MattermostPushClient` 인터페이스에만 의존한다. - **설명**: Mattermost push 통합에서 `apps/client` host가 plugin adapter에게 위임/공급해야 하는 데이터와 콜백. plugin adapter는 platform 측 plugin singleton에 격리되어 있고, 그 외 코드는 모두 `MattermostPushClient` 인터페이스에만 의존한다.
- **호스트 소유 필드/콜백**: - **호스트 소유 필드/콜백**:
- `server_url`: Mattermost 서버 base URL (auth 핸드오프, signing key 저장에 함께 사용) - `server_url`: Mattermost 서버 base URL (auth 핸드오프, signing key 저장에 함께 사용)

View file

@ -9,6 +9,21 @@ import (
"time" "time"
) )
type ProjectSyncSetting struct {
ID int64 `json:"id"`
Provider string `json:"provider"`
Tenant string `json:"tenant"`
Project string `json:"project"`
GitRemoteUrl string `json:"git_remote_url"`
SourceBranch string `json:"source_branch"`
WorkspaceID string `json:"workspace_id"`
WorkspaceBasePath string `json:"workspace_base_path"`
RepoDirName string `json:"repo_dir_name"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Task struct { type Task struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`

View file

@ -0,0 +1,129 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: project_sync_settings.sql
package db
import (
"context"
)
const createProjectSyncSetting = `-- name: CreateProjectSyncSetting :one
INSERT INTO project_sync_settings (
provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, TRUE)
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at
`
type CreateProjectSyncSettingParams struct {
Provider string `json:"provider"`
Tenant string `json:"tenant"`
Project string `json:"project"`
GitRemoteUrl string `json:"git_remote_url"`
SourceBranch string `json:"source_branch"`
WorkspaceID string `json:"workspace_id"`
WorkspaceBasePath string `json:"workspace_base_path"`
RepoDirName string `json:"repo_dir_name"`
}
func (q *Queries) CreateProjectSyncSetting(ctx context.Context, arg CreateProjectSyncSettingParams) (ProjectSyncSetting, error) {
row := q.db.QueryRow(ctx, createProjectSyncSetting,
arg.Provider,
arg.Tenant,
arg.Project,
arg.GitRemoteUrl,
arg.SourceBranch,
arg.WorkspaceID,
arg.WorkspaceBasePath,
arg.RepoDirName,
)
var i ProjectSyncSetting
err := row.Scan(
&i.ID,
&i.Provider,
&i.Tenant,
&i.Project,
&i.GitRemoteUrl,
&i.SourceBranch,
&i.WorkspaceID,
&i.WorkspaceBasePath,
&i.RepoDirName,
&i.Active,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deactivateProjectSyncSetting = `-- name: DeactivateProjectSyncSetting :one
UPDATE project_sync_settings
SET active = FALSE, updated_at = now()
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at
`
type DeactivateProjectSyncSettingParams struct {
Provider string `json:"provider"`
Tenant string `json:"tenant"`
Project string `json:"project"`
}
func (q *Queries) DeactivateProjectSyncSetting(ctx context.Context, arg DeactivateProjectSyncSettingParams) (ProjectSyncSetting, error) {
row := q.db.QueryRow(ctx, deactivateProjectSyncSetting, arg.Provider, arg.Tenant, arg.Project)
var i ProjectSyncSetting
err := row.Scan(
&i.ID,
&i.Provider,
&i.Tenant,
&i.Project,
&i.GitRemoteUrl,
&i.SourceBranch,
&i.WorkspaceID,
&i.WorkspaceBasePath,
&i.RepoDirName,
&i.Active,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getActiveProjectSyncSettingByTarget = `-- name: GetActiveProjectSyncSettingByTarget :one
SELECT id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at
FROM project_sync_settings
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE
`
type GetActiveProjectSyncSettingByTargetParams struct {
Provider string `json:"provider"`
Tenant string `json:"tenant"`
Project string `json:"project"`
}
func (q *Queries) GetActiveProjectSyncSettingByTarget(ctx context.Context, arg GetActiveProjectSyncSettingByTargetParams) (ProjectSyncSetting, error) {
row := q.db.QueryRow(ctx, getActiveProjectSyncSettingByTarget, arg.Provider, arg.Tenant, arg.Project)
var i ProjectSyncSetting
err := row.Scan(
&i.ID,
&i.Provider,
&i.Tenant,
&i.Project,
&i.GitRemoteUrl,
&i.SourceBranch,
&i.WorkspaceID,
&i.WorkspaceBasePath,
&i.RepoDirName,
&i.Active,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View file

@ -0,0 +1,213 @@
package projectsync
import (
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/nomadcode/nomadcode-core/internal/db"
"github.com/nomadcode/nomadcode-core/internal/workitem"
)
var ErrInvalidConfig = errors.New("invalid project sync config")
const (
DefaultSlotIndex SlotIndex = 0
MaxSlotIndex SlotIndex = 999
)
type ProviderProjectTarget struct {
Provider workitem.ProviderID
Tenant string
Project string
}
type Config struct {
Target ProviderProjectTarget
GitRemoteURL string
SourceBranch string
WorkspaceID string
WorkspaceBasePath string
RepoDirName string
}
type SlotIndex int
type SlotState string
const (
SlotStateAvailable SlotState = "available"
SlotStateInUse SlotState = "in_use"
SlotStateDirty SlotState = "dirty"
SlotStateError SlotState = "error"
)
func (t ProviderProjectTarget) Normalize() (ProviderProjectTarget, error) {
target := ProviderProjectTarget{
Provider: workitem.ProviderID(strings.TrimSpace(string(t.Provider))),
Tenant: strings.TrimSpace(t.Tenant),
Project: strings.TrimSpace(t.Project),
}
if target.Provider == "" || target.Tenant == "" || target.Project == "" {
return ProviderProjectTarget{}, ErrInvalidConfig
}
return target, nil
}
func (c Config) Normalize() (Config, error) {
target, err := c.Target.Normalize()
if err != nil {
return Config{}, err
}
basePath, err := normalizePath(c.WorkspaceBasePath)
if err != nil {
return Config{}, err
}
repoDir, err := normalizeRepoDirName(c.RepoDirName)
if err != nil {
return Config{}, err
}
config := Config{
Target: target,
GitRemoteURL: strings.TrimSpace(c.GitRemoteURL),
SourceBranch: strings.TrimSpace(c.SourceBranch),
WorkspaceID: strings.TrimSpace(c.WorkspaceID),
WorkspaceBasePath: basePath,
RepoDirName: repoDir,
}
if config.GitRemoteURL == "" || config.SourceBranch == "" || config.WorkspaceID == "" {
return Config{}, ErrInvalidConfig
}
return config, nil
}
func (c Config) ProjectWorkspaceRoot() (string, error) {
config, err := c.Normalize()
if err != nil {
return "", err
}
return ProjectWorkspaceRoot(config.WorkspaceBasePath, config.RepoDirName)
}
func (c Config) SlotWorkspacePath(slot SlotIndex) (string, error) {
config, err := c.Normalize()
if err != nil {
return "", err
}
return SlotWorkspacePath(config.WorkspaceBasePath, config.RepoDirName, slot)
}
func NewSlotIndex(index int) (SlotIndex, error) {
slot := SlotIndex(index)
if !slot.Valid() {
return 0, ErrInvalidConfig
}
return slot, nil
}
func (s SlotIndex) Valid() bool {
return s >= DefaultSlotIndex && s <= MaxSlotIndex
}
func (s SlotIndex) String() string {
return fmt.Sprintf("%03d", int(s))
}
func (s SlotIndex) PathName() (string, error) {
if !s.Valid() {
return "", ErrInvalidConfig
}
return s.String(), nil
}
func (s SlotState) Valid() bool {
switch s {
case SlotStateAvailable, SlotStateInUse, SlotStateDirty, SlotStateError:
return true
default:
return false
}
}
func (s SlotState) Allocatable() bool {
return s == SlotStateAvailable
}
func ProjectWorkspaceRoot(workspaceBasePath, repoDirName string) (string, error) {
basePath, err := normalizePath(workspaceBasePath)
if err != nil {
return "", err
}
repoDir, err := normalizeRepoDirName(repoDirName)
if err != nil {
return "", err
}
return filepath.Join(basePath, repoDir), nil
}
func SlotWorkspacePath(workspaceBasePath, repoDirName string, slot SlotIndex) (string, error) {
slotName, err := slot.PathName()
if err != nil {
return "", err
}
root, err := ProjectWorkspaceRoot(workspaceBasePath, repoDirName)
if err != nil {
return "", err
}
return filepath.Join(root, slotName), nil
}
func normalizePath(value string) (string, error) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "", ErrInvalidConfig
}
cleaned := filepath.Clean(trimmed)
if cleaned == "." {
return "", ErrInvalidConfig
}
return cleaned, nil
}
func normalizeRepoDirName(value string) (string, error) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "", ErrInvalidConfig
}
cleaned := filepath.Clean(trimmed)
if cleaned == "." || cleaned == ".." || filepath.IsAbs(cleaned) || strings.ContainsAny(cleaned, `/\`) {
return "", ErrInvalidConfig
}
return cleaned, nil
}
func (c Config) ToCreateProjectSyncParams() db.CreateProjectSyncSettingParams {
return db.CreateProjectSyncSettingParams{
Provider: string(c.Target.Provider),
Tenant: c.Target.Tenant,
Project: c.Target.Project,
GitRemoteUrl: c.GitRemoteURL,
SourceBranch: c.SourceBranch,
WorkspaceID: c.WorkspaceID,
WorkspaceBasePath: c.WorkspaceBasePath,
RepoDirName: c.RepoDirName,
}
}
func ConfigFromDBRecord(r db.ProjectSyncSetting) Config {
return Config{
Target: ProviderProjectTarget{
Provider: workitem.ProviderID(r.Provider),
Tenant: r.Tenant,
Project: r.Project,
},
GitRemoteURL: r.GitRemoteUrl,
SourceBranch: r.SourceBranch,
WorkspaceID: r.WorkspaceID,
WorkspaceBasePath: r.WorkspaceBasePath,
RepoDirName: r.RepoDirName,
}
}

View file

@ -0,0 +1,288 @@
package projectsync
import (
"errors"
"testing"
"time"
"github.com/nomadcode/nomadcode-core/internal/db"
"github.com/nomadcode/nomadcode-core/internal/workitem"
)
func TestConfigNormalizeRequiresProjectSyncFields(t *testing.T) {
input := Config{
Target: ProviderProjectTarget{
Provider: workitem.ProviderID(" plane "),
Tenant: " general ",
Project: " project-1 ",
},
GitRemoteURL: " git@example.com:nomadcode.git ",
SourceBranch: " develop ",
WorkspaceID: " main ",
WorkspaceBasePath: " /home/user/workspace/ ",
RepoDirName: " nomadcode ",
}
got, err := input.Normalize()
if err != nil {
t.Fatalf("Normalize returned error: %v", err)
}
if got.Target.Provider != "plane" {
t.Errorf("provider: got %q", got.Target.Provider)
}
if got.Target.Tenant != "general" {
t.Errorf("tenant: got %q", got.Target.Tenant)
}
if got.Target.Project != "project-1" {
t.Errorf("project: got %q", got.Target.Project)
}
if got.GitRemoteURL != "git@example.com:nomadcode.git" {
t.Errorf("git remote: got %q", got.GitRemoteURL)
}
if got.SourceBranch != "develop" {
t.Errorf("source branch: got %q", got.SourceBranch)
}
if got.WorkspaceID != "main" {
t.Errorf("workspace id: got %q", got.WorkspaceID)
}
if got.WorkspaceBasePath != "/home/user/workspace" {
t.Errorf("workspace base path: got %q", got.WorkspaceBasePath)
}
if got.RepoDirName != "nomadcode" {
t.Errorf("repo dir name: got %q", got.RepoDirName)
}
}
func TestConfigNormalizeRejectsMissingRequiredFields(t *testing.T) {
valid := Config{
Target: ProviderProjectTarget{
Provider: "plane",
Tenant: "general",
Project: "project-1",
},
GitRemoteURL: "git@example.com:nomadcode.git",
SourceBranch: "develop",
WorkspaceID: "main",
WorkspaceBasePath: "/home/user/workspace",
RepoDirName: "nomadcode",
}
tests := map[string]Config{
"provider": func() Config { c := valid; c.Target.Provider = ""; return c }(),
"tenant": func() Config { c := valid; c.Target.Tenant = ""; return c }(),
"project": func() Config { c := valid; c.Target.Project = ""; return c }(),
"git remote": func() Config { c := valid; c.GitRemoteURL = ""; return c }(),
"source branch": func() Config { c := valid; c.SourceBranch = ""; return c }(),
"workspace id": func() Config { c := valid; c.WorkspaceID = ""; return c }(),
"workspace base path": func() Config { c := valid; c.WorkspaceBasePath = ""; return c }(),
"repo dir name": func() Config { c := valid; c.RepoDirName = ""; return c }(),
}
for name, input := range tests {
t.Run(name, func(t *testing.T) {
_, err := input.Normalize()
if !errors.Is(err, ErrInvalidConfig) {
t.Fatalf("Normalize error: got %v, want ErrInvalidConfig", err)
}
})
}
}
func TestWorkspacePathsUseRepoRootAndZeroPaddedSlot(t *testing.T) {
root, err := ProjectWorkspaceRoot("/home/user/workspace", "nomadcode")
if err != nil {
t.Fatalf("ProjectWorkspaceRoot returned error: %v", err)
}
if root != "/home/user/workspace/nomadcode" {
t.Fatalf("root: got %q", root)
}
slotPath, err := SlotWorkspacePath("/home/user/workspace", "nomadcode", DefaultSlotIndex)
if err != nil {
t.Fatalf("SlotWorkspacePath returned error: %v", err)
}
if slotPath != "/home/user/workspace/nomadcode/000" {
t.Fatalf("slot path: got %q", slotPath)
}
}
func TestSlotIndexFormattingAndBounds(t *testing.T) {
slot, err := NewSlotIndex(2)
if err != nil {
t.Fatalf("NewSlotIndex returned error: %v", err)
}
if slot.String() != "002" {
t.Fatalf("slot string: got %q", slot.String())
}
for _, index := range []int{-1, 1000} {
t.Run("invalid", func(t *testing.T) {
if _, err := NewSlotIndex(index); !errors.Is(err, ErrInvalidConfig) {
t.Fatalf("NewSlotIndex(%d) error: got %v, want ErrInvalidConfig", index, err)
}
})
}
}
func TestSlotStateValidationAndAllocation(t *testing.T) {
if !SlotStateAvailable.Valid() || !SlotStateInUse.Valid() || !SlotStateDirty.Valid() || !SlotStateError.Valid() {
t.Fatal("expected known slot states to be valid")
}
if !SlotStateAvailable.Allocatable() {
t.Fatal("available slot should be allocatable")
}
if SlotStateInUse.Allocatable() || SlotStateDirty.Allocatable() || SlotStateError.Allocatable() {
t.Fatal("only available slots should be allocatable")
}
if SlotState("unknown").Valid() {
t.Fatal("unknown slot state should be invalid")
}
}
func TestRepoDirNameRejectsNestedOrAbsolutePaths(t *testing.T) {
for _, repoDirName := range []string{"/nomadcode", "../nomadcode", "team/nomadcode", `team\nomadcode`} {
t.Run(repoDirName, func(t *testing.T) {
_, err := ProjectWorkspaceRoot("/home/user/workspace", repoDirName)
if !errors.Is(err, ErrInvalidConfig) {
t.Fatalf("ProjectWorkspaceRoot error: got %v, want ErrInvalidConfig", err)
}
})
}
}
// TestConfigToCreateProjectSyncParams verifies the DB params conversion.
func TestConfigToCreateProjectSyncParams(t *testing.T) {
config := Config{
Target: ProviderProjectTarget{
Provider: "plane",
Tenant: "general",
Project: "project-x",
},
GitRemoteURL: "git@gh.com:org/repo.git",
SourceBranch: "main",
WorkspaceID: "ws-abc",
WorkspaceBasePath: "/data/ws",
RepoDirName: "repo",
}
// Normalize first (required by contract)
normal, err := config.Normalize()
if err != nil {
t.Fatalf("Normalize failed: %v", err)
}
params := normal.ToCreateProjectSyncParams()
if params.Provider != "plane" {
t.Errorf("Provider: got %q", params.Provider)
}
if params.Tenant != "general" {
t.Errorf("Tenant: got %q", params.Tenant)
}
if params.Project != "project-x" {
t.Errorf("Project: got %q", params.Project)
}
if params.GitRemoteUrl != "git@gh.com:org/repo.git" {
t.Errorf("GitRemoteUrl: got %q", params.GitRemoteUrl)
}
if params.SourceBranch != "main" {
t.Errorf("SourceBranch: got %q", params.SourceBranch)
}
if params.WorkspaceID != "ws-abc" {
t.Errorf("WorkspaceID: got %q", params.WorkspaceID)
}
if params.WorkspaceBasePath != "/data/ws" {
t.Errorf("WorkspaceBasePath: got %q", params.WorkspaceBasePath)
}
if params.RepoDirName != "repo" {
t.Errorf("RepoDirName: got %q", params.RepoDirName)
}
}
// TestConfigFromDBRecord verifies the DB record → Config conversion.
func TestConfigFromDBRecord(t *testing.T) {
now := time.Now()
record := db.ProjectSyncSetting{
ID: 7,
Provider: "plane",
Tenant: "org2",
Project: "proj-y",
GitRemoteUrl: "git@bitbucket.com:team/app.git",
SourceBranch: "develop",
WorkspaceID: "ws-def",
WorkspaceBasePath: "/srv/ws",
RepoDirName: "app",
Active: true,
CreatedAt: now,
UpdatedAt: now,
}
cfg := ConfigFromDBRecord(record)
if cfg.Target.Provider != workitem.ProviderID("plane") {
t.Errorf("Target.Provider: got %q", cfg.Target.Provider)
}
if cfg.Target.Tenant != "org2" {
t.Errorf("Target.Tenant: got %q", cfg.Target.Tenant)
}
if cfg.Target.Project != "proj-y" {
t.Errorf("Target.Project: got %q", cfg.Target.Project)
}
if cfg.GitRemoteURL != "git@bitbucket.com:team/app.git" {
t.Errorf("GitRemoteURL: got %q", cfg.GitRemoteURL)
}
if cfg.SourceBranch != "develop" {
t.Errorf("SourceBranch: got %q", cfg.SourceBranch)
}
if cfg.WorkspaceID != "ws-def" {
t.Errorf("WorkspaceID: got %q", cfg.WorkspaceID)
}
if cfg.WorkspaceBasePath != "/srv/ws" {
t.Errorf("WorkspaceBasePath: got %q", cfg.WorkspaceBasePath)
}
if cfg.RepoDirName != "app" {
t.Errorf("RepoDirName: got %q", cfg.RepoDirName)
}
}
// TestConfigRoundTrip verifies Config → DB params → Config preserves fields.
func TestConfigRoundTrip(t *testing.T) {
original := Config{
Target: ProviderProjectTarget{
Provider: "jira",
Tenant: "acme",
Project: "board-42",
},
GitRemoteURL: "git@github.com:acme/core.git",
SourceBranch: "release/v1",
WorkspaceID: "ws-rt",
WorkspaceBasePath: "/mnt/ws",
RepoDirName: "core",
}
normal, err := original.Normalize()
if err != nil {
t.Fatalf("Normalize: %v", err)
}
params := normal.ToCreateProjectSyncParams()
record := db.ProjectSyncSetting{
ID: 99,
Provider: params.Provider,
Tenant: params.Tenant,
Project: params.Project,
GitRemoteUrl: params.GitRemoteUrl,
SourceBranch: params.SourceBranch,
WorkspaceID: params.WorkspaceID,
WorkspaceBasePath: params.WorkspaceBasePath,
RepoDirName: params.RepoDirName,
Active: true,
}
recovered := ConfigFromDBRecord(record)
if recovered.Target.Provider != normal.Target.Provider {
t.Errorf("Provider round-trip: got %q", recovered.Target.Provider)
}
if recovered.GitRemoteURL != normal.GitRemoteURL {
t.Errorf("GitRemoteURL round-trip: got %q", recovered.GitRemoteURL)
}
}

View file

@ -0,0 +1,44 @@
package storage
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/nomadcode/nomadcode-core/internal/db"
)
var (
ErrProjectSyncNotFound = errors.New("project sync setting not found")
)
// CreateProjectSyncRecord inserts a new project sync setting row as active.
// It expects normalized fields from the callers.
func (s *Store) CreateProjectSyncRecord(ctx context.Context, args db.CreateProjectSyncSettingParams) (db.ProjectSyncSetting, error) {
return s.queries.CreateProjectSyncSetting(ctx, args)
}
// GetActiveProjectSyncSettingByTarget retrieves the active project sync config for a provider/project target.
func (s *Store) GetActiveProjectSyncSettingByTarget(ctx context.Context, provider string, tenant string, project string) (db.ProjectSyncSetting, error) {
result, err := s.queries.GetActiveProjectSyncSettingByTarget(ctx, db.GetActiveProjectSyncSettingByTargetParams{
Provider: provider,
Tenant: tenant,
Project: project,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return db.ProjectSyncSetting{}, ErrProjectSyncNotFound
}
return db.ProjectSyncSetting{}, err
}
return result, nil
}
// DeactivateProjectSyncSetting marks the active config for a target as inactive.
func (s *Store) DeactivateProjectSyncSetting(ctx context.Context, provider string, tenant string, project string) (db.ProjectSyncSetting, error) {
return s.queries.DeactivateProjectSyncSetting(ctx, db.DeactivateProjectSyncSettingParams{
Provider: provider,
Tenant: tenant,
Project: project,
})
}

View file

@ -0,0 +1,443 @@
package storage
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/nomadcode/nomadcode-core/internal/db"
)
// SQL template keys matching the const strings in db/project_sync_settings.sql.go
const (
sqlCreateKey = `-- name: CreateProjectSyncSetting :one
INSERT INTO project_sync_settings (
provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, TRUE)
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at
`
sqlGetActiveKey = `-- name: GetActiveProjectSyncSettingByTarget :one
SELECT id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at
FROM project_sync_settings
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE
`
sqlDeactKey = `-- name: DeactivateProjectSyncSetting :one
UPDATE project_sync_settings
SET active = FALSE, updated_at = now()
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at
`
)
type queryCall struct {
sql string
args []interface{}
}
type mockDBTX struct {
queries map[string]fakeQueryResult
queryLog []string
queryCalls []queryCall
}
type fakeQueryResult struct {
row db.ProjectSyncSetting
err error
}
func (m *mockDBTX) Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) {
return pgconn.NewCommandTag("OK"), nil
}
func (m *mockDBTX) Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
return nil, errors.New("implement me")
}
func (m *mockDBTX) QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row {
m.queryLog = append(m.queryLog, sql)
m.queryCalls = append(m.queryCalls, queryCall{sql: sql, args: args})
if r, ok := m.queries[sql]; ok {
return &mockRow{result: r}
}
return &mockRow{result: fakeQueryResult{err: pgx.ErrNoRows}}
}
type mockRow struct {
result fakeQueryResult
}
func (f *mockRow) Scan(dest ...interface{}) error {
if f.result.err != nil {
return f.result.err
}
// Scan the saved row into dest pointers (simulates real pgx.Row.Scan).
row := f.result.row
dests := dest
if len(dests) >= 12 {
if p, ok := dests[0].(*int64); ok {
*p = row.ID
}
if p, ok := dests[1].(*string); ok {
*p = row.Provider
}
if p, ok := dests[2].(*string); ok {
*p = row.Tenant
}
if p, ok := dests[3].(*string); ok {
*p = row.Project
}
if p, ok := dests[4].(*string); ok {
*p = row.GitRemoteUrl
}
if p, ok := dests[5].(*string); ok {
*p = row.SourceBranch
}
if p, ok := dests[6].(*string); ok {
*p = row.WorkspaceID
}
if p, ok := dests[7].(*string); ok {
*p = row.WorkspaceBasePath
}
if p, ok := dests[8].(*string); ok {
*p = row.RepoDirName
}
if p, ok := dests[9].(*bool); ok {
*p = row.Active
}
if p, ok := dests[10].(*time.Time); ok {
*p = row.CreatedAt
}
if p, ok := dests[11].(*time.Time); ok {
*p = row.UpdatedAt
}
}
return nil
}
func sqlContains(logs []string, sub string) bool {
for _, l := range logs {
if strings.Contains(l, sub) {
return true
}
}
return false
}
// assertArgsAt asserts that the recorded args at the given index match expected values.
func assertArgsAt(t *testing.T, calls []queryCall, idx int, expected []interface{}, label string) {
t.Helper()
if idx >= len(calls) {
t.Fatalf("[%s] expected call at index %d, but only %d calls recorded", label, idx, len(calls))
}
got := calls[idx].args
if len(got) != len(expected) {
t.Errorf("[%s] call[%d] args: expected %d args, got %d: %v", label, idx, len(expected), len(got), got)
return
}
for i := range expected {
if !reflect.DeepEqual(got[i], expected[i]) {
t.Errorf("[%s] call[%d] args[%d]: expected %v (type %T), got %v (type %T)",
label, idx, i, expected[i], expected[i], got[i], got[i])
}
}
}
func TestProjectSyncQueryCompile(t *testing.T) {
_ = pgx.ErrNoRows
}
func TestCreateProjectSyncSettingParams(t *testing.T) {
p := db.CreateProjectSyncSettingParams{
Provider: "plane", Tenant: "general", Project: "project-1",
GitRemoteUrl: "git@example.com:nomadcode.git", SourceBranch: "main",
WorkspaceID: "ws-1", WorkspaceBasePath: "/home/user/workspace", RepoDirName: "nomadcode",
}
if p.Provider != "plane" {
t.Errorf("Provider: got %q", p.Provider)
}
if p.GitRemoteUrl != "git@example.com:nomadcode.git" {
t.Errorf("GitRemoteUrl: got %q", p.GitRemoteUrl)
}
}
func TestGetActiveProjectSyncSettingByTargetParams(t *testing.T) {
p := db.GetActiveProjectSyncSettingByTargetParams{
Provider: "plane", Tenant: "general", Project: "project-1",
}
if p.Provider != "plane" || p.Tenant != "general" || p.Project != "project-1" {
t.Error("unexpected params")
}
}
func TestDeactivateProjectSyncSettingParams(t *testing.T) {
p := db.DeactivateProjectSyncSettingParams{
Provider: "plane", Tenant: "general", Project: "project-1",
}
if p.Provider != "plane" || p.Tenant != "general" || p.Project != "project-1" {
t.Error("unexpected params")
}
}
func TestErrProjectSyncNotFoundIsDefined(t *testing.T) {
err := ErrProjectSyncNotFound
if err == nil {
t.Fatal("ErrProjectSyncNotFound should not be nil")
}
if err.Error() != "project sync setting not found" {
t.Errorf("error message: got %q", err.Error())
}
if err == pgx.ErrNoRows {
t.Error("ErrProjectSyncNotFound should not equal pgx.ErrNoRows")
}
}
func TestProjectSyncSettingModelShape(t *testing.T) {
now := time.Now()
r := db.ProjectSyncSetting{
ID: 42, Provider: "plane", Tenant: "general", Project: "project-1",
GitRemoteUrl: "git@example.com:repo.git", SourceBranch: "dev",
WorkspaceID: "ws-test", WorkspaceBasePath: "/opt/ws", RepoDirName: "repo",
Active: true, CreatedAt: now, UpdatedAt: now,
}
if r.ID != 42 {
t.Errorf("ID: got %d", r.ID)
}
if !r.Active {
t.Error("Active should be true")
}
if r.WorkspaceBasePath != "/opt/ws" {
t.Errorf("WorkspaceBasePath: got %q", r.WorkspaceBasePath)
}
}
// ------ REVIEW_API-1: error boundary tests -
func TestGetActiveProjectSyncSettingByTarget_ReturnsErrProjectSyncNotFound_OnNoRows(t *testing.T) {
m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {err: pgx.ErrNoRows}}}
queries := db.New(m)
store := NewStoreWithQueries(queries)
_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "tenant-1", "proj-1")
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrProjectSyncNotFound) {
t.Errorf("expected ErrProjectSyncNotFound, got %v", err)
}
}
func TestGetActiveProjectSyncSettingByTarget_PreservesGenericDBError(t *testing.T) {
sentinel := errors.New("database timeout")
m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {err: sentinel}}}
queries := db.New(m)
store := NewStoreWithQueries(queries)
_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "tenant-1", "proj-1")
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, sentinel) {
t.Errorf("expected sentinel DB error %v, got %v", sentinel, err)
}
}
// ------ G08: Assert storage query args -
func TestCreateProjectSyncRecord_CallsQueryWithCorrectArgs(t *testing.T) {
expectRow := db.ProjectSyncSetting{ID: 1, Active: true}
m := &mockDBTX{
queries: map[string]fakeQueryResult{
sqlCreateKey: {row: expectRow},
sqlGetActiveKey: {err: pgx.ErrNoRows},
sqlDeactKey: {err: errors.New("nope")},
},
}
queries := db.New(m)
store := NewStoreWithQueries(queries)
params := db.CreateProjectSyncSettingParams{
Provider: "bitbucket", Tenant: "acme", Project: "main-app",
GitRemoteUrl: "git@bitbucket.org:acme/main-app.git", SourceBranch: "main",
WorkspaceID: "ws-1", WorkspaceBasePath: "/opt/ws", RepoDirName: "main-app",
}
result, err := store.CreateProjectSyncRecord(context.Background(), params)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.ID != 1 {
t.Errorf("expected ID 1, got %d", result.ID)
}
if !result.Active {
t.Error("expected Active=true")
}
if len(m.queryLog) == 0 {
t.Fatal("expected at least one QueryRow call, got none")
}
if !sqlContains(m.queryLog, "INSERT INTO project_sync_settings") {
t.Errorf("expected INSERT query, queryLog=%v", m.queryLog)
}
// Assert args for create: [provider, tenant, project, git_remote_url, source_branch, workspace_id, workspace_base_path, repo_dir_name]
assertArgsAt(t, m.queryCalls, 0, []interface{}{
"bitbucket", "acme", "main-app",
"git@bitbucket.org:acme/main-app.git", "main",
"ws-1", "/opt/ws", "main-app",
}, "CreateProjectSyncRecord")
}
func TestGetActiveProjectSyncSettingByTarget_CallsQueryWithCorrectArgs(t *testing.T) {
testRow := db.ProjectSyncSetting{ID: 42, Active: true}
m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {row: testRow}}}
queries := db.New(m)
store := NewStoreWithQueries(queries)
_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "tenant-1", "proj-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(m.queryCalls) == 0 {
t.Fatal("expected QueryRow call")
}
// Assert args for get: [provider, tenant, project]
assertArgsAt(t, m.queryCalls, 0, []interface{}{
"plan", "tenant-1", "proj-1",
}, "GetActiveProjectSyncSettingByTarget")
}
func TestDeactivateProjectSyncSetting_CallsQueryWithCorrectArgs(t *testing.T) {
deactivatedRow := db.ProjectSyncSetting{ID: 99, Active: false}
m := &mockDBTX{queries: map[string]fakeQueryResult{sqlDeactKey: {row: deactivatedRow}}}
queries := db.New(m)
store := NewStoreWithQueries(queries)
_, err := store.DeactivateProjectSyncSetting(context.Background(), "plan", "t1", "p1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(m.queryCalls) == 0 {
t.Fatal("expected QueryRow call")
}
// Assert args for deactivate: [provider, tenant, project]
assertArgsAt(t, m.queryCalls, 0, []interface{}{
"plan", "t1", "p1",
}, "DeactivateProjectSyncSetting")
}
func TestCreateProjectSyncRecord_GetsNotFoundOnMissingRecord(t *testing.T) {
m2 := &mockDBTX{
queries: map[string]fakeQueryResult{
sqlDeactKey: {err: pgx.ErrNoRows},
},
}
queries2 := db.New(m2)
store2 := NewStoreWithQueries(queries2)
_, err := store2.DeactivateProjectSyncSetting(context.Background(), "x", "y", "z")
// DeactivateProjectSyncSetting does NOT convert ErrNoRows to ErrProjectSyncNotFound.
// It returns the raw pgx.ErrNoRows.
if !errors.Is(err, pgx.ErrNoRows) {
t.Fatalf("expected pgx.ErrNoRows, got %v", err)
}
// Verify deactivation was called with the correct args.
assertArgsAt(t, m2.queryCalls, 0, []interface{}{
"x", "y", "z",
}, "DeactivateProjectSyncSetting (after create)")
}
func TestGetActiveProjectSyncSettingByTarget_ReturnsActiveRecord(t *testing.T) {
testRow := db.ProjectSyncSetting{ID: 42, Active: true}
m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {row: testRow}}}
queries := db.New(m)
store := NewStoreWithQueries(queries)
result, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "t1", "p1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.ID != 42 {
t.Errorf("expected ID 42, got %d", result.ID)
}
if len(m.queryLog) == 0 {
t.Fatal("expected QueryRow call")
}
if !sqlContains(m.queryLog, "active = TRUE") {
t.Error("expected active lookup guard (active = TRUE) in query")
}
}
func TestGetActiveProjectSyncSettingByTarget_InactiveExcluded(t *testing.T) {
m := &mockDBTX{queries: map[string]fakeQueryResult{sqlGetActiveKey: {err: pgx.ErrNoRows}}}
queries := db.New(m)
store := NewStoreWithQueries(queries)
_, err := store.GetActiveProjectSyncSettingByTarget(context.Background(), "plan", "t1", "p1")
if err == nil {
t.Fatal("expected error for inactive/absent config")
}
if !errors.Is(err, ErrProjectSyncNotFound) {
t.Errorf("expected ErrProjectSyncNotFound, got %v", err)
}
}
func TestDeactivateProjectSyncSetting_CallsQueryWithActiveGuard(t *testing.T) {
deactivatedRow := db.ProjectSyncSetting{ID: 99, Active: false}
m := &mockDBTX{queries: map[string]fakeQueryResult{sqlDeactKey: {row: deactivatedRow}}}
queries := db.New(m)
store := NewStoreWithQueries(queries)
result, err := store.DeactivateProjectSyncSetting(context.Background(), "plan", "t1", "p1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Active {
t.Error("expected Active=false after deactivation, got true")
}
if len(m.queryLog) == 0 {
t.Fatal("expected QueryRow call")
}
if !sqlContains(m.queryLog, "active = FALSE") {
t.Error("expected SET active = FALSE guard in deactivate query")
}
}
func TestStoreCalls_PassProviderTenantProjectParams(t *testing.T) {
testRow := db.ProjectSyncSetting{ID: 1, Active: true}
m := &mockDBTX{
queries: map[string]fakeQueryResult{
sqlCreateKey: {row: testRow},
sqlGetActiveKey: {row: testRow},
sqlDeactKey: {row: db.ProjectSyncSetting{ID: 1, Active: false}},
},
}
queries := db.New(m)
store := NewStoreWithQueries(queries)
store.CreateProjectSyncRecord(context.Background(), db.CreateProjectSyncSettingParams{
Provider: "github", Tenant: "org-x", Project: "auth",
GitRemoteUrl: "git@github.com:x/feat.git", SourceBranch: "main",
WorkspaceID: "ws-1", WorkspaceBasePath: "/ws", RepoDirName: "feat",
})
store.GetActiveProjectSyncSettingByTarget(context.Background(), "github", "org-x", "auth")
store.DeactivateProjectSyncSetting(context.Background(), "github", "org-x", "auth")
if len(m.queryCalls) < 3 {
t.Fatalf("expected at least 3 QueryRow calls, got %d", len(m.queryCalls))
}
// Call 0: CreateProjectSyncSetting — args: [provider, tenant, project, git_remote_url, source_branch, workspace_id, workspace_base_path, repo_dir_name]
assertArgsAt(t, m.queryCalls, 0, []interface{}{
"github", "org-x", "auth",
"git@github.com:x/feat.git", "main",
"ws-1", "/ws", "feat",
}, "CreateProjectSyncRecord")
// Call 1: GetActiveProjectSyncSettingByTarget — args: [provider, tenant, project]
assertArgsAt(t, m.queryCalls, 1, []interface{}{
"github", "org-x", "auth",
}, "GetActiveProjectSyncSettingByTarget")
// Call 2: DeactivateProjectSyncSetting — args: [provider, tenant, project]
assertArgsAt(t, m.queryCalls, 2, []interface{}{
"github", "org-x", "auth",
}, "DeactivateProjectSyncSetting")
}

View file

@ -29,6 +29,11 @@ func NewStore(pool *pgxpool.Pool) *Store {
return &Store{queries: db.New(pool)} return &Store{queries: db.New(pool)}
} }
// NewStoreWithQueries creates a Store with a pre-built *db.Queries (for testing).
func NewStoreWithQueries(queries *db.Queries) *Store {
return &Store{queries: queries}
}
func (s *Store) CreateTask(ctx context.Context, input CreateTaskInput) (db.Task, error) { func (s *Store) CreateTask(ctx context.Context, input CreateTaskInput) (db.Task, error) {
metadata := input.Metadata metadata := input.Metadata
if len(metadata) == 0 || string(metadata) == "null" { if len(metadata) == 0 || string(metadata) == "null" {

View file

@ -0,0 +1,27 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS project_sync_settings (
id BIGSERIAL PRIMARY KEY,
provider TEXT NOT NULL,
tenant TEXT NOT NULL,
project TEXT NOT NULL,
git_remote_url TEXT NOT NULL,
source_branch TEXT NOT NULL,
workspace_id TEXT NOT NULL,
workspace_base_path TEXT NOT NULL,
repo_dir_name TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Each provider/tenant/project combo has at most one active config.
CREATE UNIQUE INDEX ux_project_sync_active
ON project_sync_settings (provider, tenant, project)
WHERE active = TRUE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS project_sync_settings;
-- +goose StatementEnd

View file

@ -0,0 +1,23 @@
-- name: CreateProjectSyncSetting :one
INSERT INTO project_sync_settings (
provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, TRUE)
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at;
-- name: GetActiveProjectSyncSettingByTarget :one
SELECT id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at
FROM project_sync_settings
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE;
-- name: DeactivateProjectSyncSetting :one
UPDATE project_sync_settings
SET active = FALSE, updated_at = now()
WHERE provider = $1 AND tenant = $2 AND project = $3 AND active = TRUE
RETURNING id, provider, tenant, project, git_remote_url, source_branch,
workspace_id, workspace_base_path, repo_dir_name, active,
created_at, updated_at;