# Flutter/Core API Contract Candidates

이 문서는 source schema가 아니라 Flutter-first 전환 중 공유 계약 후보를 모으는 compatibility note다.
실제 Go core 구현(`services/core`)과 향후 작성될 Flutter 클라이언트(`apps/client`) 간의 인터페이스 정합성을 보장하기 위한 임시 계약 후보이며, 단일 source-of-truth 스키마가 정의되기 전까지의 과도기적 스펙을 정의한다.

## API 안정성 레벨 정의
- **안정 (Stable)**: 인터페이스와 데이터 포맷이 고정되어 있어 즉시 연동에 사용할 수 있음.
- **후보 (Candidate)**: 연동 후보 스펙으로, 실연동 과정에서 필드가 추가되거나 변경될 수 있음.

---

## 1. 내부 통신 원칙

### 1.1 기본 표준
- **설명**: NomadCode 내부 런타임 간 신규 통신은 `proto-socket`을 기본 표준으로 사용한다.
- **안정성 수준**: 후보 (Candidate)
- **적용 대상**:
  - `apps/client` ↔ `services/core`
  - NomadCode Core ↔ 내부 control/runtime adapter
  - 향후 NomadCode가 직접 소유하는 MCP/action runtime 표면
- **예외 대상**:
  - health/readiness처럼 외부 운영 도구와 curl smoke가 직접 확인해야 하는 endpoint
  - Plane/Jira/Mattermost/IOP/OpenAI-compatible Responses/A2A처럼 외부 provider 또는 외부 실행 표면이 요구하는 native protocol
  - public callback/webhook
  - 단순 정적 조회처럼 `proto-socket` 연결 비용이 과한 read-only endpoint
- **예외 기록 규칙**: REST/HTTP를 새로 추가하거나 유지할 때는 이 문서 또는 관련 domain rule에 예외 사유를 남긴다.

### 1.2 proto-socket Semantic Envelope 후보
- **설명**: 아래 JSON은 wire schema가 아니라 proto-socket message가 가져야 할 의미 필드 후보다. 실제 source schema는 proto-socket 계약이 안정화되면 별도 schema로 승격한다.
- **Envelope Shape 후보**:
  ```json
  {
    "protocol_version": "nomadcode.proto-socket.v1",
    "id": "msg-uuid",
    "correlation_id": "request-or-flow-id",
    "type": "request",
    "channel": "task",
    "action": "task.list",
    "actor": {
      "kind": "client",
      "id": "apps/client"
    },
    "auth": {
      "scheme": "basic",
      "token_ref": "runtime-secret-ref"
    },
    "payload": {},
    "error": null,
    "meta": {
      "connection_id": "conn-id",
      "timestamp": "2026-05-30T00:00:00Z"
    }
  }
  ```
- **type 후보**:
  - `request`: 요청 메시지
  - `response`: 요청 성공 응답
  - `event`: 구독 또는 서버 발행 이벤트
  - `error`: 요청 실패 또는 channel-level 오류
  - `heartbeat`: 연결 생존 확인
  - `auth`: 연결 인증 또는 인증 갱신
- **error 후보 필드**:
  ```json
  {
    "code": "task.not_found",
    "message": "task not found",
    "retryable": false,
    "details": {}
  }
  ```

### 1.3 REST Compatibility Map 후보
- **설명**: 현재 REST API는 proto-socket rail 구현 전까지의 호환 표면이다. task 관련 신규 내부 기능은 proto-socket channel을 우선한다.

| 현재 REST/HTTP 표면 | proto-socket 후보 | 유지/전환 기준 |
|--------------------|-------------------|----------------|
| `GET /healthz` | 없음 | Public 운영 health check로 REST 유지 |
| `GET /readyz` | `system.ready` event 또는 request 후보 | DB readiness와 curl smoke 때문에 REST 유지, 내부 UI에는 proto-socket 상태 이벤트를 추가 가능 |
| `POST /api/tasks` | `task.create` request | client-core 내부 호출은 proto-socket으로 전환, REST는 smoke/compat 유지 |
| `GET /api/tasks` | `task.list` request | client-core 내부 조회는 proto-socket으로 전환, REST는 smoke/compat 유지 |
| `GET /api/tasks/{id}` | `task.get` request | client-core 내부 조회는 proto-socket으로 전환, REST는 smoke/compat 유지 |
| `POST /api/tasks/{id}/enqueue` | `task.enqueue` request + `task.status.changed` event | client-core 실행 요청과 상태 반영은 proto-socket으로 전환, REST는 smoke/compat 유지 |
| `POST /api/integrations/plane/tasks` | `workitem.plane.import` 후보 | Plane API 자체는 외부 provider REST이며, 내부 호출 표면은 proto-socket으로 추가 가능 |

### 1.4 Core Endpoint 후보
- **기본 경로**: `/proto-socket`
- **인증**: 기존 Core Basic Auth middleware 적용
- **Wire message**: 초기 구현은 `google.protobuf.Struct` semantic envelope를 사용한다.
- **Heartbeat**: proto-socket 기본 heartbeat를 사용하며 Core 설정 기본값은 interval 30s, wait 10s다.

### 1.4.1 Workspace port와 endpoint compatibility 후보
- **설명**: 이 표는 source schema가 아니라 NomadCode workspace/remote runner에서 core, Flutter preview, provider endpoint를 같은 언어로 다루기 위한 compatibility note다.

| 표면 | 후보 기준 | compatibility note |
|------|-----------|--------------------|
| Flutter web preview | code-server `/proxy/<port>/` 아래 remote local `13010`, remote dev `13011` host port | 기존 local smoke의 ad hoc `8081` 계열 포트는 계속 허용한다. |
| code-server workspace entry | `http://localhost:8080/?folder=...` mock URL 유지 | 이 URL은 code-server entry 호환성이고 Flutter web preview 포트가 아니다. |
| Core HTTP/API | process/container 내부 `8080`, remote local host publish `18010:8080`, remote dev host publish `18011:8080` | `HTTP_ADDR=:8080`, Docker `EXPOSE 8080`, local curl `localhost:8080`은 migration compatibility로 유지하고, compose는 `NOMADCODE_CORE_HOST_PORT`로 host port를 override한다. |
| PostgreSQL | compose/service DNS `code-server-postgres:5432`; host 필요 시 local `15410:5432`, dev `15411:5432` 후보 | 기본 내부 통신은 service DNS를 우선하고 host publish는 추가하지 않는다. |
| Redis | compose/service DNS `code-server-redis:6379`; host 필요 시 local `16310:6379`, dev `16311:6379` 후보 | 기본 내부 통신은 service DNS를 우선하고 host publish는 추가하지 않는다. |
| proto-socket client/core endpoint | client는 `PROTO_SOCKET_*`, core는 `/proto-socket`과 heartbeat env를 사용 | client `PROTO_SOCKET_HOST`가 없으면 connector는 비활성화된다. |
| IOP Edge/provider endpoints | `MODEL_*`, `A2A_*`, `PLANE_*`, `JIRA_*`, `MATTERMOST_*` env group | raw token, password, API key 값은 tracked contract note에 기록하지 않는다. |

### 1.5 Task Channel Actions (구현됨)
- **설명**: `services/core/internal/protosocket`가 REST task API와 같은 의미를 `task` channel의 proto-socket action으로 제공한다. REST는 smoke/compat로 유지한다.
- **안정성 수준**: 후보 (Candidate)
- **채널**: `task`
- **요청/응답 action 표**:

| action | request payload | response type | response payload |
|--------|-----------------|---------------|------------------|
| `task.create` | `POST /api/tasks` JSON shape (`title`, `source`, `payload`, `metadata?`, `external?`) | `response` | `{ "id", "status", "external_provider", "external_id", "task": { …full task } }` |
| `task.list` | `{ "limit": 20 }` (생략 시 0 전달, 기본 20/최대 100 bounding은 core workflow service가 담당) | `response` | `{ "tasks": [ { …full task } ] }` |
| `task.get` | `{ "id": "task-id" }` | `response` | `{ "task": { …full task } }` |
| `task.enqueue` | `{ "id": "task-id" }` | `response` | `{ "id", "status": "queued", "task": { …full task } }` |

- **task status event (서버 발행)**:

| action | type | channel | payload |
|--------|------|---------|---------|
| `task.status.changed` | `event` | `task` | `{ "id", "type": "task.running\|task.completed\|task.failed\|task.canceled", "status", "title", "message", "attempt?", "reason?", "occurred_at?" }` |

  - scheduler가 내는 모든 task lifecycle event(running/completed/failed/canceled)가 notification fanout을 통해 broadcast된다. completed event의 Mattermost 알림 동작은 그대로 유지된다.
  - `attempt` (int), `reason` (string), `occurred_at` (ISO8601 string) 등의 workflow context가 payload에 포함된다.

- **error envelope code 표** (`type: "error"`):

| code | 의미 | retryable | REST 대응 status |
|------|------|-----------|------------------|
| `task.invalid_input` | 잘못된 task 입력(`ErrInvalidTaskInput`) | false | 400 |
| `task.conflict` | 현재 상태에서 enqueue 불가(`ErrTaskCannotBeEnqueued`) | false | 409 |
| `task.not_found` | task 없음(`pgx.ErrNoRows`) | false | 404 |
| `task.invalid_payload` | envelope payload 디코드 실패 | false | (REST 없음) |
| `internal.error` | 그 외 내부 오류 | true | 500 |
| `UNSUPPORTED_ACTION` | dispatcher에 등록되지 않은 action | false | (REST 없음) |

### 1.6 Diagnostics 필드 후보
- **설명**: proto-socket 로그와 클라이언트 디버그 표면에서 연결과 메시지 흐름을 추적하기 위한 공통 diagnostics 필드 후보다. 확정 schema가 아니라 Core/Client 구현을 맞추기 위한 compatibility note로만 사용한다.
- **안정성 수준**: 후보 (Candidate)
- **최소 공통 필드**:

| 필드 | 위치 | 필수성 | 소비자 책임 |
|------|------|--------|-------------|
| `connection_id` | envelope `meta.connection_id`, Core log attribute, Client debug surface | 연결이 확립된 response/event/error에서 가능하면 포함 | Core는 연결별 안정 식별자를 부여하고, Client는 표시/필터링에만 사용한다. |
| `protocol_version` | envelope top-level, Core log attribute, Client debug surface | 모든 proto-socket envelope에 포함 | Core와 Client는 지원하지 않는 version을 compatibility 문제로 보고하되, secret이나 payload 원문을 함께 출력하지 않는다. |
| `channel` | envelope top-level, Core log attribute, Client debug surface | request/response/event/error에서 포함 | dispatcher와 Client debug surface는 channel name을 그대로 노출하되 business payload를 함께 노출하지 않는다. |
| `action` | envelope top-level, Core log attribute, Client debug surface | request/response/event/error에서 포함 | request 흐름과 최근 event/error를 추적하는 용도로만 사용한다. |
| `error.code` / `error_code` | envelope `error.code`, Core log attribute, Client debug surface | error envelope에서 포함, 성공 envelope에서는 생략 또는 null | Core는 error code를 안정적인 문자열로 기록하고, Client는 최근 오류 요약에만 사용한다. |
| `timestamp` | envelope `meta.timestamp`, Core log timestamp, Client debug surface | Core가 발행하는 response/event/error에서 가능하면 포함 | 소비자는 시간 정렬과 최근 상태 표시에 사용하고 clock 차이를 프로토콜 오류로 단정하지 않는다. |

- **Core log attributes**: `connection_id`, `protocol_version`, `channel`, `action`, `error_code`, `timestamp`를 structured log attribute로 남긴다. error가 없으면 `error_code`는 생략하거나 null로 둔다.
- **Envelope meta**: response/event/error는 가능한 경우 `meta.connection_id`, `meta.timestamp`를 포함하고, top-level `protocol_version`, `channel`, `action`과 함께 diagnostics 기준으로 소비된다.
- **Client debug surface**: 연결 상태, connection id, protocol version, 최근 channel/action/error code/timestamp만 표시한다.
- **노출 금지**: auth token, Basic Auth header, secret, `token_ref`가 가리키는 실제 값, payload/result 원문, 외부 provider credential은 Core log와 Client debug surface에 기록하거나 표시하지 않는다.

### 1.7 새 내부 통신 추가 운영 체크
- 신규 `apps/client` ↔ `services/core` 또는 내부 runtime 간 통신은 먼저 proto-socket channel/action으로 표현 가능한지 확인한다.
- REST/HTTP를 새로 추가하거나 유지해야 하면 health/readiness, 외부 provider 호환 API, 운영 smoke/curl, public callback, 단순 정적 조회 중 어느 예외인지 이 문서 또는 관련 domain rule에 기록한다.
- 새 channel/action은 envelope의 `protocol_version`, `type`, `channel`, `action`, `correlation_id`, `payload`, `error.retryable` 의미를 이 문서에 추가한다.
- 클라이언트가 소비하는 request/response/event shape는 구현 전에 후보 표와 테스트 fixture 기준을 남긴다.
- Core 구현은 기존 REST smoke/compat 표면을 제거하지 않고, 내부 UI/런타임 호출만 proto-socket 우선 경로로 전환한다.
- 검증은 최소한 channel/action dispatch, auth boundary, error envelope, event broadcast 또는 reconnect 영향 중 해당되는 항목을 테스트로 고정한다.

### 1.8 외부 Gito Branch Event Consumer
- **설명**: NomadCode는 Gito의 branch event를 HTTP webhook으로 수신하는 소비자입니다. 소비 표면은 Core backend bridge가 canonical이며, Flutter client wakeup은 후속 UI projection 책임으로 분리합니다.
- **NomadCode 소비 계약**: `agent-contract/outer/gito-branch-webhook-consumer-v1.md` (계약 ID: `nomadcode.gito-branch-webhook-consumer.v1`)
- **Gito 원문 제공 계약**: Gito `agent-contract/provided/gito-forgejo-branch-events-v1.md` (계약 ID: `gito.forgejo-branch-events.v1`)
- **소비 표면**: `POST /api/integrations/gito/webhook` 및 `branch.updated` payload.
- **포트 profile**: remote local은 Gito `18060` -> NomadCode `18010`, remote dev는 Gito `18061` -> NomadCode `18011`을 기준으로 한다.
- **공통 책임 경계**: NomadCode는 `branch.updated` 이벤트를 단순 wakeup signal로만 해석하고, Plane/roadmap mutation 전 target branch revision에 대한 재검증 및 후속 동기화 여부를 직접 확인해야 합니다. `GITO_PROTO_SOCKET_URL`은 이전 wire consumer 호환 경로가 필요할 때만 사용합니다.

#### 1.8.1 Flutter client wakeup
- **소비자**: `apps/client` Flutter 앱.
- **용도**: Core가 처리한 branch event projection을 UI wakeup 힌트로만 사용합니다. 화면 갱신/알림 트리거에 쓰며, 자체적으로 source-of-truth 상태를 변경하지 않습니다.

#### 1.8.2 Core backend bridge
- **소비자**: `services/core` HTTP Gito webhook consumer (`POST /api/integrations/gito/webhook`) 및 내부 Gito branch event bridge.
- **용도**: Core는 Gito 제공 계약 `gito.forgejo-branch-events.v1`을 소비해 `branch.updated`를 backend sync enqueue wakeup으로만 사용합니다.
- **`changed_files` 취급**: `changed_files`는 roadmap Milestone 관련성 판단을 위한 wakeup 힌트이며, 그 자체를 신뢰해 동기화하지 않습니다. 실제 target branch revision 확인/스캔은 backend bridge(후속 revision-scan 단계)의 책임입니다.
- **구독 필터**: `repo_id`, `branch`로 구독하며, 다른 repo/branch event는 enqueue 후보로 넘기지 않습니다.

---


## 2. 공통 및 인프라 API

### 2.1 `GET /healthz`
- **설명**: 서비스 헬스 체크.
- **인증**: 없음 (Public)
- **안정성 수준**: 안정 (Stable)
- **Response Shape** (Status 200 OK):
  ```json
  {
    "status": "ok"
  }
  ```

### 2.2 `GET /readyz`
- **설명**: 서비스 및 데이터베이스 준비 상태 체크.
- **인증**: Basic Auth
- **안정성 수준**: 안정 (Stable)
- **Response Shape** (Status 200 OK):
  ```json
  {
    "status": "ready"
  }
  ```
  *(Status 503 Service Unavailable)*:
  ```json
  {
    "error": "database is not ready"
  }
  ```

---

## 3. 태스크 관리 API (Tasks API)

### 3.1 `POST /api/tasks`
- **설명**: 새로운 태스크 생성.
- **인증**: Basic Auth
- **안정성 수준**: 안정 (Stable)
- **Request Shape**:
  ```json
  {
    "title": "Task Title",
    "source": "source_identifier",
    "payload": {},
    "metadata": {},
    "external": {
      "provider": "plane",
      "id": "external-task-id",
      "url": "https://...",
      "metadata": {}
    }
  }
  ```
- **Response Shape** (Status 201 Created):
  ```json
  {
    "id": "task-uuid-string",
    "status": "pending",
    "external_provider": "plane",
    "external_id": "external-task-id"
  }
  ```

### 3.2 `GET /api/tasks`
- **설명**: 태스크 목록 조회.
- **인증**: Basic Auth
- **안정성 수준**: 안정 (Stable)
- **Query Parameters**:
  - `limit` (Optional, 기본값 20, 양의 정수)
- **Response Shape** (Status 200 OK):
  ```json
  [
    {
      "id": "task-uuid-string",
      "title": "Task Title",
      "source": "source_identifier",
      "status": "pending",
      "payload": {},
      "result": {},
      "error": null,
      "created_at": "2026-05-24T12:00:00Z",
      "updated_at": "2026-05-24T12:00:00Z",
      "external_provider": "plane",
      "external_id": "external-task-id",
      "external_url": "https://...",
      "external_metadata": {},
      "metadata": {}
    }
  ]
  ```

### 3.3 `GET /api/tasks/{id}`
- **설명**: 단일 태스크 세부 정보 조회.
- **인증**: Basic Auth
- **안정성 수준**: 안정 (Stable)
- **Response Shape** (Status 200 OK):
  ```json
  {
    "id": "task-uuid-string",
    "title": "Task Title",
    "source": "source_identifier",
    "status": "pending",
    "payload": {},
    "result": {},
    "error": null,
    "created_at": "2026-05-24T12:00:00Z",
    "updated_at": "2026-05-24T12:00:00Z",
    "external_provider": "plane",
    "external_id": "external-task-id",
    "external_url": "https://...",
    "external_metadata": {},
    "metadata": {}
  }
  ```

### 3.4 `POST /api/tasks/{id}/enqueue`
- **설명**: 태스크를 큐에 진입시켜 실행 대기 상태로 변경.
- **인증**: Basic Auth
- **안정성 수준**: 안정 (Stable)
- **Response Shape** (Status 200 OK):
  ```json
  {
    "id": "task-uuid-string",
    "status": "queued"
  }
  ```

---

## 4. 외부 연동 API (Integrations API)

### 4.1 `POST /api/integrations/plane/tasks`
- **설명**: Plane 워크아이템 정보를 기반으로 새로운 태스크 생성.
- **인증**: Basic Auth
- **안정성 수준**: 안정 (Stable)
- **Request Shape**:
  ```json
  {
    "workspace_slug": "my-workspace",
    "project_id": "project-uuid-string",
    "work_item_id": "work-item-id-string",
    "state_id": "state-uuid-string",
    "external_url": "https://...",
    "comment": "Optional comment"
  }
  ```
- **Response Shape** (Status 201 Created):
  ```json
  {
    "id": "task-uuid-string",
    "status": "pending",
    "external_provider": "plane",
    "external_id": "work-item-id-string"
  }
  ```

---

## 5. Roadmap / Work Item Sync 계약 후보

> [!IMPORTANT]
> 이 절은 source schema가 아니라 Plane/Jira 같은 provider work item과 `agent-roadmap` Milestone item을 같은 구조로 유지하기 위한 sync domain 계약 후보입니다. 실제 구현 전까지는 compatibility note로만 사용합니다.

### 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. Roadmap Operations Control Plane 계약 후보

> [!IMPORTANT]
> 이 절은 Roadmap Operations Control Plane의 후보 계약입니다. 실제 source schema가 아니며, Core가 roadmap/action side effect를 소유하도록 만들 때의 compatibility note로만 사용합니다.

### 6.1 Roadmap State Model 후보
- **설명**: Core는 `agent-roadmap` 파일을 직접 덮어쓰는 도구가 아니라, roadmap state를 읽고 revision/idempotency 조건을 확인한 뒤 action 결과를 제안하거나 적용하는 소유자다.
- **상태 단위**:
  - `roadmap`: 전체 Phase 흐름, source branch, current revision.
  - `phase`: Phase 상태, Phase 문서 경로, Milestone 흐름, archive link.
  - `milestone`: Milestone 상태, 구현 잠금, SDD gate, 기능 Task 상태, 완료 리뷰.
  - `task_artifact`: `agent-task/m-<milestone-slug>`의 plan, code-review, `USER_REVIEW.md`, `complete.log`.
  - `review_gate`: 사용자 입력, 출력 검토, 완료 승인, archive 승인처럼 자동 적용 전에 멈춰야 하는 판단 지점.
- **공통 revision 필드 후보**:

| 필드 | 설명 |
|------|------|
| `roadmap_revision` | Core가 읽은 git commit SHA 또는 file/index revision |
| `document_path` | `ROADMAP.md`, `PHASE.md`, Milestone 문서, SDD 문서, task artifact 경로 |
| `document_hash` | 같은 commit 안에서 파일 단위 충돌을 확인하기 위한 content hash 후보 |
| `expected_revision` | caller가 적용을 기대하는 revision |
| `idempotency_key` | 같은 command 재시도 중복 적용 방지 키 |
| `actor` | 사용자, provider webhook, scheduler, agent, runtime event 같은 변경 주체 |
| `reason` | 사용자 요청, provider status change, completion event, reconciliation 등 적용 사유 |

### 6.2 Roadmap Action 후보
- **설명**: Flutter UI, provider webhook, scheduler, agent runtime은 같은 Core action shape를 통해 roadmap 변경을 검사하거나 적용한다.

| action | request payload | response payload |
|--------|-----------------|------------------|
| `roadmap.validate` | `{ "scope?", "paths?", "strict": true }` | `{ "valid", "diagnostics", "revision" }` |
| `roadmap.get_position` | `{ "current_path?", "include_flow": true }` | `{ "roadmap", "phase", "milestones", "current_candidates" }` |
| `roadmap.propose_change` | `{ "target", "change", "actor", "reason", "dry_run": true }` | `{ "proposal", "affected_paths", "review_gate?" }` |
| `roadmap.apply_change` | `{ "proposal_id?", "target", "change", "expected_revision", "idempotency_key", "actor", "reason" }` | `{ "applied", "revision", "affected_paths", "review_gate?" }` |
| `roadmap.transition` | `{ "target", "to_status", "expected_revision", "idempotency_key", "actor", "reason" }` | `{ "transitioned", "from_status", "to_status", "revision", "review_gate?" }` |
| `roadmap.archive` | `{ "target", "expected_revision", "idempotency_key", "actor", "reason", "approved": false }` | `{ "archived", "archive_paths", "revision", "review_gate?" }` |
| `roadmap.dependency_check` | `{ "target", "direction": "both" }` | `{ "locks", "ready", "blocking_reasons" }` |
| `roadmap.ingest_completion_event` | `{ "origin_task", "complete_log", "roadmap_completion", "expected_revision?", "idempotency_key" }` | `{ "applied_tasks", "milestone_status?", "revision", "review_gate?" }` |

- **공통 입력 원칙**:
  - 상태 변경, archive, dependency lock 갱신, completion event ingest는 `expected_revision`과 `idempotency_key`를 받는다.
  - destructive 변경과 archive는 사용자 승인 또는 `review_gate` 해소 없이 자동 적용하지 않는다.
  - `roadmap.propose_change`는 문서 초안이나 dry-run proposal을 만들 수 있지만, 적용은 `roadmap.apply_change` 또는 더 구체적인 transition/archive action이 담당한다.
  - `roadmap.ingest_completion_event`는 `complete.log`의 `Roadmap Completion`과 Milestone 기능 Task id가 매칭될 때만 Task 체크 후보를 만든다.

### 6.3 Review Gate 후보
- **설명**: Core는 자동 판단이 위험한 지점을 review gate로 올리고, gate가 해소되기 전까지 apply/archive side effect를 멈춘다.

| gate | trigger | resolution |
|------|---------|------------|
| `user_input_required` | 제품 방향, 범위, 책임 경계처럼 에이전트가 확정할 수 없는 결정 | Milestone `구현 잠금 > 결정 필요` 또는 SDD `USER_REVIEW.md` |
| `task_user_review` | plan/code-review 루프가 사용자 결정 없이는 이어갈 수 없음 | `agent-task/.../USER_REVIEW.md` 해소 후 후속 completion event |
| `completion_review` | 모든 기능 Task가 충족되어 `[검토중]` 전환 후보 | 사용자 완료 확인 |
| `archive_approval` | `[완료]` 전환과 archive 이동 후보 | 사용자 archive 승인 |
| `conflict_review` | revision mismatch, provider/roadmap 동시 수정, destructive change | dry-run proposal 확인 후 명시 apply |

- Plane `User Review`는 provider board state이고, `USER_REVIEW.md`는 agent-task 또는 SDD의 stop artifact다. Core는 둘을 같은 사용자 판단 신호로 정규화하되 저장 위치와 해소 조건은 분리한다.

### 6.4 HTTP 역할과 첫 구현 순서 후보
- HTTP API는 Flutter UI, provider webhook, internal integration, smoke/curl compatibility 표면으로 유지한다.
- HTTP handler는 roadmap 문서를 직접 변경하지 않고 Core action을 호출하며, action request에 `expected_revision`, `idempotency_key`, `actor`, `reason`, `dry_run`을 전달한다.
- 외부 provider webhook은 wakeup/input channel이고, roadmap mutation 여부는 Core action의 validation과 revision check 결과에 따른다.
- 첫 구현 순서는 `roadmap.validate` -> `roadmap.get_position` -> `roadmap.ingest_completion_event` 순서를 기본 후보로 둔다.
  - `roadmap.validate`: side effect 없이 문서 구조와 lock/gate 상태를 확인한다.
  - `roadmap.get_position`: Flutter UI와 agent runtime이 같은 current/flow 해석을 공유한다.
  - `roadmap.ingest_completion_event`: 기존 plan/code-review 완료 이벤트를 Core action 경계로 끌어와 Milestone Task 체크 후보를 만든다.

---

## 7. 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 저장 대상으로 본다.

### 7.1 Workspace Metadata 후보 스펙
- **설명**: 에이전트 작업 공간(Workspace)에 대한 설정 및 상태 메타데이터.
- **후보 필드**:
  - `workspace_id`: 작업 공간 고유 식별자 (UUID)
  - `workspace_slug`: 작업 공간의 URL 친화적 식별자 (예: `nomadcode-dev`)
  - `name`: 작업 공간 표시 이름 (예: `NomadCode Dev Workspace`)
  - `owner_id`: 소유자 고유 식별자
  - `created_at`: 생성 시각 (ISO 8601)
  - `updated_at`: 최종 변경 시각 (ISO 8601)
  - `status`: 현재 상태 (예: `active`, `archived`, `suspended`)
  - `settings`: 작업 공간에 특화된 동적 설정 JSON 오브젝트 (예: 자동 스케줄링 옵션, 알림 채널 정보 등)

### 7.2 Project Metadata 후보 스펙
- **설명**: 작업 공간 내 세부 프로젝트(Project) 정보.
- **후보 필드**:
  - `project_id`: 프로젝트 고유 식별자 (UUID)
  - `workspace_id`: 해당 프로젝트가 속한 Workspace 식별자
  - `name`: 프로젝트 표시 이름 (예: `Flutter client consolidation`)
  - `description`: 프로젝트 세부 설명
  - `provider`: 연동 플랫폼 식별자 (예: `plane`, `github`)
  - `external_project_id`: 연동 플랫폼 측 프로젝트 ID
  - `status`: 프로젝트 상태 (예: `active`, `paused`)

---

## 8. 클라이언트 Integration 설정 계약 후보

> [!IMPORTANT]
> 이 절은 `apps/client`의 integration boundary에서 host/plugin/transport 사이에 교환되는 설정값을 후보로 모은 것이다. 실제 source schema는 소비하는 core/client 경계가 안정화된 뒤로 미룬다.
> 관련 코드 경계: `apps/client/lib/src/integrations/proto_socket/` 와 `apps/client/lib/src/integrations/mattermost/`.

### 8.1 proto-socket Endpoint 설정 후보
- **설명**: `apps/client`가 proto-socket transport에 의존할 때 사용하는 연결/하트비트 설정. 현재 구현은 `proto_socket` Dart 패키지(local path)를 끌어 쓰며 bootstrap에서 자동 연결하지 않는다.
- **후보 필드**:
  - `host`: 대상 서버 hostname
  - `port`: TCP/WebSocket 포트 (기본은 `secure` 값에 따라 443 또는 80)
  - `secure`: TLS 사용 여부 (boolean)
  - `path`: WebSocket path (기본 `/`)
  - `heartbeat_interval_seconds`: heartbeat 송신 주기 (기본 30)
  - `heartbeat_wait_seconds`: heartbeat 응답 대기 한계 (기본 60)
  - `enabled`: bootstrap 시 자동 연결 활성화 여부 (기본 false)

### 8.2 Mattermost Push Host 책임 경계 후보
- **설명**: Mattermost push 통합에서 `apps/client` host가 plugin adapter에게 위임/공급해야 하는 데이터와 콜백. plugin adapter는 platform 측 plugin singleton에 격리되어 있고, 그 외 코드는 모두 `MattermostPushClient` 인터페이스에만 의존한다.
- **호스트 소유 필드/콜백**:
  - `server_url`: Mattermost 서버 base URL (auth 핸드오프, signing key 저장에 함께 사용)
  - `server_identifier`: 선택 필드. FCM payload의 `server_id`를 `server_url`로 역해석해야 하는 multi-server host에서 plugin의 `setAuthToken(..., identifier:)`로 함께 전달
  - `auth_token`: 로그인 응답에서 받은 인증 토큰 (plugin의 `setAuthToken`으로 전달)
  - `signing_key`: 서버 config의 `AsymmetricSigningPublicKey` (plugin의 `setSigningKey`로 전달)
  - `device_token`: FCM 디바이스 토큰 (plugin의 `getDeviceToken` 및 `onDeviceTokenReady` 콜백으로 동기화)
  - `on_navigate_to_channel(server_url, channel_id)`: notification open → 채널 라우팅 콜백
  - `on_navigate_to_thread(server_url, root_id)`: notification open → CRT 스레드 라우팅 콜백
  - `notification_stream`: app-level UI(snackbar 등) 소비를 위한 broadcast stream
