--- domain: core last_rule_review_commit: 03187a4be3d9864419221d14bb44b8bc6645e6f1 last_rule_updated_at: 2026-06-01 --- # core ## 목적 / 책임 NomadCode의 백엔드 오케스트레이션 도메인이다. workflow, scheduling, persistence, HTTP/proto-socket API, work item intake, 외부 서비스 adapter를 통해 AI 작업 실행 상태와 자동화 흐름을 관리한다. ## 포함 경로 - `services/core/cmd/` — 서버와 Plane smoke 실행 entrypoint. - `services/core/internal/agent/` — agent 모델과 실행 단위. - `services/core/internal/config/` — 환경 변수 기반 core 설정 로딩. - `services/core/internal/model/` — 모델 호출 추상 인터페이스와 입출력 타입. - `services/core/internal/workflow/` — workflow 모델과 서비스. - `services/core/internal/scheduler/` — River 기반 작업 스케줄링과 job 정의. - `services/core/internal/notification/` — task notification 모델과 발행 서비스. - `services/core/internal/http/` — HTTP router, handlers, middleware. - `services/core/internal/protosocket/` — proto-socket envelope, dispatcher, server, task channel, event broadcaster. - `services/core/internal/workitem/` — provider-neutral work item reference, capability, projection, task input mapping. - `services/core/internal/workitempipeline/` — 외부 work item을 core task로 변환하는 pipeline service. - `services/core/internal/db/` — SQLC 생성 코드, DB pool, persistence 모델. - `services/core/internal/storage/` — 저장소 abstraction. - `services/core/internal/adapters/` — OpenAI, A2A, Mattermost, Plane 등 외부 통합. - `services/core/migrations/`, `services/core/queries/`, `services/core/sqlc.yaml` — DB schema/query 계약. - `services/core/go.mod`, `services/core/go.sum` — core Go module dependency boundary. - `services/core/README.md`, `services/core/goose.env.example` — core 실행/운영 문서와 설정 예시. - `services/core/bin/`, `services/core/Makefile`, `services/core/Dockerfile`, `services/core/docker-compose.yml` — backend-local 개발/배포 도구. ## 제외 경로 - `apps/client/` — Flutter-first 제품 클라이언트이며 core 내부 패키지를 직접 참조하지 않는다. - `apps/web/` — [Legacy/Retired] React/Vite scaffold 경로는 완전히 제거되어 제외 대상입니다. - `packages/contracts/` — 공유 API/schema 계약의 소유 영역이다. ## 주요 구성 요소 - `cmd/server/main.go` — core server wiring과 adapter/proto-socket 조립. - `cmd/plane-smoke/main.go` — Plane lookup/comment/status projection smoke runner. - `internal/http/router.go` — HTTP route 조립. - `internal/http/handlers.go` — task API와 Plane work item intake handler. - `internal/config/config.go` — 환경 변수 기반 설정 로딩. - `internal/model/model.go` — 모델 client interface와 generation DTO. - `internal/scheduler/river.go` — River scheduler 연결. - `internal/scheduler/jobs.go` — background job 정의. - `internal/notification/service.go` — task 완료 notification 발행. - `internal/protosocket/server.go` — proto-socket WebSocket server, heartbeat, diagnostics logging, broadcast. - `internal/protosocket/dispatcher.go` — channel/action dispatch registry. - `internal/protosocket/envelope.go` — `nomadcode.proto-socket.v1` semantic envelope 변환. - `internal/protosocket/tasks.go` — `task.create/list/get/enqueue` proto-socket channel handlers. - `internal/protosocket/events.go` — notification event를 `task.status.changed` event로 투영. - `internal/adapters/openai/client.go` — OpenAI integration. - `internal/adapters/a2a/client.go` — A2A integration. - `internal/adapters/mattermost/client.go` — Mattermost task notification integration. - `internal/adapters/plane/client.go` — Plane work item 조회, comment 생성, state update integration. - `internal/workitem/provider.go` — provider-neutral work item abstraction과 task input mapping. - `internal/workitempipeline/service.go` — work item reader와 workflow task creator 연결. - `internal/db/tasks.sql.go` — SQLC generated query boundary. ## Plane dev 작업 메모 - dev Plane URL은 `https://plane.toki-labs.com`이다. - dev Plane 서버 확인이 필요하면 `ssh toki@toki-labs.com`으로 접속하고, Plane compose 위치는 `~/docker/agent-service/plane/compose`다. - 원격 SSH의 기본 shell에서는 `docker`가 PATH에 없을 수 있으므로 `zsh -lc`로 실행한다. 예: `cd ~/docker/agent-service/plane/compose && docker compose ps`. - compose service 이름은 `plane-api`, `plane-worker`, `plane-beat`, `plane-frontend`, `postgres`, `redis`, `rabbitmq`, `minio`다. 실제 컨테이너 이름은 `plane-api`, `plane-worker`, `plane-beat`, `plane-frontend`, `plane-postgres`, `plane-redis`, `plane-rabbitmq`, `plane-minio`다. - NomadCode dev Plane workspace slug는 `general`, workspace id는 `dadf050e-cd1e-4590-bc33-672511630841`, project id는 `a6beb42f-7a8a-410c-b50f-ea3ca94828f3`, project identifier는 `NOMAD`다. - NomadCode project state id: Backlog `62d4c50c-0cea-4a76-a0ed-ec97498b2d5f`, Todo `45ba7449-f684-4381-af6d-5854747c5e8d`, In Progress `c6ac1a6b-74d5-47fb-8b36-646d2bf0284d`, User Review `dc4fb920-f763-4ae3-9569-3c5e2d500391`, Done `ea2e5b48-8bf1-4723-b749-de7723be41e9`, Cancelled `f29c06c2-d70c-4b56-a83c-fccc4db60ae4`. - Plane API 호출은 repo root의 ignored local secret file `.env.plane.local`을 우선 사용한다. 이 파일은 `PLANE_BASE_URL`, `PLANE_TOKEN`, `PLANE_WORKSPACE_SLUG`, `PLANE_PROJECT_ID`를 담고 있으며 `.gitignore`로 제외된다. shell 작업에서는 `set -a; source .env.plane.local; set +a`로 로드한다. - `.env.plane.local`의 현재 Plane token 계정은 NomadCode project에서 Admin role로 설정되어 있다. - Plane webhook secret은 SOPS encrypted file `secrets/nomadcode.dev.sops.yaml`의 `PLANE_WEBHOOK_SECRET`으로 관리하고, runtime에는 같은 이름의 env로 주입한다. dev Plane UI에서 발급한 webhook secret 원문은 domain rule/roadmap/docs/로그에 기록하지 않는다. - Plane API token, webhook secret, 서버 `.env`의 secret 값은 domain rule에 직접 기록하지 않는다. 토큰 파일이 없거나 API 권한이 부족할 때만 서버 내부 조작이 명시적으로 요청되면 `plane-api`의 Django shell/ORM을 사용한다. - Plane custom property는 work item types 기반 기능이며, 현재 NomadCode dev project의 `is_issue_type_enabled`는 `False`다. 무료 self-hosted/free tier에서는 이 기능을 전제로 설계하지 않는다. - Plane work item 샘플 `NOMAD-13`은 `In Progress` state에 있으며, board state와 agent 내부 실행 상태를 분리하는 예시다. external source/id는 `nomadcode` / `sample-agent-in-progress-state`이고, 라벨 `agent:waiting-user`, `phase:planning`으로 내부 상태를 보드에서 보이게 한다. - Plane control flow의 board state는 `backlog`, `todo`, `in_progress`, `user_review`, `done`, `cancelled`로 둔다. canonical agent 실행 상태는 core task metadata에 저장하고, provider에는 labels를 우선 투영하며, 자세한 사유는 comment로 남긴다. - Plane `Todo + AGENT assignee`는 Milestone 초안 생성/갱신 gate이며 자동 실행 gate가 아니다. 사용자가 상위 티켓을 `In Progress`로 옮길 때 Milestone 하위 작업 생성과 실행을 시작한다. - Milestone에서 만들어진 Plane 하위 티켓은 정상 PASS 시 기본적으로 `Done`으로 이동한다. 사용자 결정, 외부 환경, 범위 충돌처럼 자동 진행이 막힐 때만 `User Review`로 이동한다. - Plane과 Jira 모두 provider workflow status는 가볍게 유지한다. agent 내부 실행 상태는 우선 labels로 투영하고, Jira에서는 필요 시 issue property/custom field를 보조 저장소로 쓸 수 있게 adapter 경계를 둔다. ## 유지할 패턴 - Go package 경계는 `internal/` 단위로 유지한다. - DB 변경은 migration, query, generated code, test를 함께 갱신한다. - 외부 서비스 호출은 `internal/adapters/` 아래에 격리한다. - provider-neutral work item 계약은 `internal/workitem`에 두고, Plane/Jira 같은 provider별 HTTP/DTO 세부는 adapter 아래에 둔다. - work item을 core task로 만드는 흐름은 `internal/workitempipeline`에서 조립하고, workflow service에 provider별 DTO를 직접 넣지 않는다. - 신규 내부 런타임 통신은 `proto-socket` channel/action을 우선 검토하고, REST/HTTP 예외는 health/readiness, smoke/curl, 외부 provider 호환 API처럼 사유가 분명할 때 contracts 또는 domain rule에 남긴다. - proto-socket envelope diagnostics에는 connection id, protocol version, channel/action, error code, timestamp 같은 운영 필드만 남기고 secret이나 payload 원문을 로그에 싣지 않는다. - 환경 변수 기본값과 alias는 `internal/config`에서 관리하고 README의 실행 예시와 어긋나지 않게 유지한다. - scheduler 변경은 가능한 한 job 단위 테스트를 추가한다. - HTTP API 변경은 Flutter 앱 영향과 contracts 반영 필요성을 함께 판단한다. - Plane 관련 작업에서는 board state와 agent 내부 실행 상태를 같은 workflow state로 섞지 않는다. `User Review`는 사용자 판단이 필요한 provider board state이고, 세부 사유는 `agent_run_state`, `wait_type`, label, comment로 표현한다. - Plane/Jira work item 본문(description)은 작업 요구사항과 맥락의 원본으로 취급하고, agent 실행 상태를 매번 갱신하는 저장소로 사용하지 않는다. ## 다른 도메인과의 경계 - **contracts**: API, proto-socket envelope/channel/action, diagnostics 의미가 클라이언트와 공유되면 contracts에 먼저 표현하거나 compatibility note로 문서화한다. - **mobile**: mobile은 public API, proto-socket semantic envelope, notification/auth 계약만 의존하며 core 내부 패키지를 직접 참조하지 않는다. - **web**: [Legacy/Retired] React web 도메인은 완전히 제거되었습니다. - **workspace-ops**: root helper 변경은 workspace-ops 소유이고, core-local bin 변경만 core 소유다. ## 금지 사항 - `apps/**`의 UI 상태나 라우팅을 core 변경에 끼워 넣지 않는다. - 외부 provider별 세부 구현을 workflow/service 레이어에 직접 흘리지 않는다. - proto-socket payload를 임의 map indexing으로 여기저기 해석하지 말고 envelope/channel boundary에서 typed DTO로 변환한다. - SQLC 생성 파일만 수동 수정하지 않는다. - 테스트 없이 scheduler, persistence, adapter behavior를 크게 바꾸지 않는다.