<!-- task=m-external-integration/03_mattermost_adapter plan=0 tag=MM -->

# Plan - MM

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성은 필수다. 검증을 실행하고 실제 내용과 출력, 계획 대비 변경 사항을 채운 뒤 active 파일을 그대로 둔 채 리뷰 준비 완료를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 범위 충돌 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정, 근거, 실행한 명령과 출력을 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 evidence 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 계획 대비 변경 사항으로 기록한다.

## 배경

첫 Epic의 `mattermost-adapter`만 active milestone에서 아직 완료 체크되지 않았다. Core에는 Mattermost REST post client, task completion notification sink, server wiring이 이미 있으나, milestone 완료에는 core unit evidence와 `agent-test/local/mattermost-server-generated-push-smoke.md` 기준의 server-generated signed push evidence가 함께 필요하다. 이 plan은 새 범위를 늘리지 않고 Mattermost adapter 완료 근거를 모아 Roadmap Completion을 남긴다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사항은 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. code-review가 요청의 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- Task ids:
  - `mattermost-adapter`: Mattermost 메시지 발송 adapter 구현과 `../nexo/packages/messaging_flutter` host notification boundary 정합성 유지
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/ROADMAP.md`
- `agent-roadmap/phase/external-integration/PHASE.md`
- `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- `agent-task/archive/2026/06/m-external-integration/01_provider_intake_registry/complete.log`
- `agent-task/archive/2026/06/m-external-integration/01_plane_adapter/complete.log`
- `agent-task/archive/2026/06/m-external-integration/02_jira_adapter/complete.log`
- `agent-task/archive/2026/06/m-external-integration/02+01_adapter_boundary/complete.log`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/mobile/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/mobile-smoke.md`
- `agent-test/local/mobile-android-sdk-smoke.md`
- `agent-test/local/mattermost-server-generated-push-smoke.md`
- `services/core/internal/adapters/mattermost/client.go`
- `services/core/internal/adapters/mattermost/client_test.go`
- `services/core/internal/adapters/mattermost/task_notification_sink.go`
- `services/core/internal/adapters/mattermost/task_notification_sink_test.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `services/core/internal/notification/model.go`
- `services/core/internal/notification/service.go`
- `services/core/internal/notification/service_test.go`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/cmd/server/main.go`
- `services/core/README.md`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` present and read. Matching profiles read: `core-smoke`, `mobile-smoke`, `mobile-android-sdk-smoke`, `mattermost-server-generated-push-smoke`.
- Core rule requires at least `cd services/core && go test ./...` for core changes. For focused Mattermost evidence, use `cd services/core && go test -count=1 ./internal/adapters/mattermost ./internal/notification ./internal/scheduler`.
- Mattermost push profile requires remote Android runner, ignored `agent-test/local/testing-secrets.md`, server-generated Mattermost REST DM or mention, redacted FCM/ACK/notification/opened/reply/dismiss evidence. Raw token, password, FCM token, signing key, push signature must not be written to tracked files or task logs.
- `<확인 필요>` remains only in baseline output examples, not in required commands. If the ignored secrets file, remote runner, sibling `../nexo/packages/messaging_flutter`, sibling `../proto-socket/dart`, or emulator is unavailable, record the exact blocker in `사용자 리뷰 요청`.

### 테스트 커버리지 공백

- Mattermost REST post request, bearer auth, body mapping, non-2xx redaction: covered by `services/core/internal/adapters/mattermost/client_test.go`.
- Task completion sink posts only completed events and propagates provider errors: covered by `services/core/internal/adapters/mattermost/task_notification_sink_test.go`.
- Scheduler emits completed notification events and logs notification sink errors without rolling back canonical task completion: covered by `services/core/internal/scheduler/jobs_test.go` plus `services/core/internal/scheduler/jobs.go`.
- Server-generated FCM path, Android notification display, ACK/opened/reply/dismiss, and Nexo host boundary: not covered by Go unit tests. Must be collected through `agent-test/local/mattermost-server-generated-push-smoke.md` evidence or blocked with exact missing prerequisite.

### 심볼 참조

- No renamed or removed symbols are planned.

### 분할 판단

- Split decision policy was evaluated before choosing plan files.
- Shared task group: `m-external-integration`.
- Existing completed siblings found by allowed same-group `complete.log` lookup: `01_provider_intake_registry`, `01_plane_adapter`, `02_jira_adapter`, `02+01_adapter_boundary`.
- This plan uses a single subtask directory `03_mattermost_adapter` because only one remaining roadmap Task id is targeted, no new shared API is introduced, expected source ownership is core Mattermost notification evidence, and the high-risk mobile/remote part is verification evidence rather than a separate code rollout.

### 범위 결정 근거

- Do not modify `../nexo/packages/messaging_flutter`; the milestone requires host boundary parity, not plugin extraction.
- Do not add Flutter app features unless the smoke evidence shows a NomadCode host wiring regression that cannot be explained by environment or secrets.
- Do not add new provider routes, Plane/Jira changes, or adapter-boundary refactors; those Task ids already have PASS `complete.log` evidence.
- Do not record raw Mattermost token, FCM token, signing key, password, or push payload signature in tracked files, task logs, or final response.

### 빌드 등급

- `cloud-G07`: core unit verification is bounded, but completion depends on remote Android runner, Mattermost/FCM smoke evidence, private secret handling, and external notification behavior.

## 구현 체크리스트

- [ ] [MM-1] Core Mattermost adapter, notification sink, scheduler fanout evidence를 재검증하고 필요한 경우에만 최소 테스트 또는 문서 보강을 한다.
- [ ] [MM-2] `agent-test/local/mattermost-server-generated-push-smoke.md` 기준으로 server-generated Mattermost push evidence를 수집하거나, 사용자 소유 환경 차단을 review stub에 기록한다. 검증: core가 Mattermost에 메시지를 발송하고, server-generated signed push smoke가 `agent-test/local/mattermost-server-generated-push-smoke.md` 기준으로 FCM/ACK/opened/reply/dismiss evidence를 남긴다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [MM-1] Core Mattermost Adapter Evidence

#### 문제

`services/core/internal/adapters/mattermost/client.go:56`는 Mattermost `POST /api/v4/posts`를 구현하고, `services/core/internal/adapters/mattermost/task_notification_sink.go:32`는 completed task event만 Mattermost로 보낸다. `services/core/cmd/server/main.go:72`는 해당 sink를 notification service에 붙이고, `services/core/internal/scheduler/jobs.go:95`와 `services/core/internal/scheduler/jobs.go:328`은 완료 이벤트를 fanout하되 sink 오류로 canonical completion을 되돌리지 않는다. 이 근거를 최신 코드 기준으로 재검증해야 `mattermost-adapter` 완료 주장에 쓸 수 있다.

#### 해결 방법

기본 구현은 유지한다. 아래 before/after는 expected no-op이며, 테스트가 실패할 때만 기존 adapter boundary 안에서 최소 수정한다.

```go
// services/core/internal/adapters/mattermost/task_notification_sink.go:32
func (s *TaskNotificationSink) HandleTaskEvent(ctx context.Context, event notification.TaskEvent) error {
	if s.client == nil {
		return nil
	}
	if event.Type != notification.TaskEventCompleted {
		return nil
	}
	return s.client.SendTaskNotification(ctx, TaskNotificationInput{
		TaskID:  event.TaskID,
		Title:   strings.TrimSpace(event.Title),
		Status:  strings.TrimSpace(event.Status),
		Message: strings.TrimSpace(event.Message),
	})
}
```

```go
// after: keep the same behavior unless focused tests expose a regression.
```

#### 수정 파일 및 체크리스트

- [ ] `services/core/internal/adapters/mattermost/client.go`: expected no source change; fix only if focused tests fail.
- [ ] `services/core/internal/adapters/mattermost/task_notification_sink.go`: expected no source change; fix only if focused tests fail.
- [ ] `services/core/internal/adapters/mattermost/client_test.go`: add or update tests only for an observed coverage gap.
- [ ] `services/core/internal/adapters/mattermost/task_notification_sink_test.go`: add or update tests only for an observed coverage gap.
- [ ] `services/core/internal/scheduler/jobs_test.go`: add regression coverage only if notification fanout behavior is not already verified by current tests.
- [ ] `services/core/README.md`: update only if the smoke/evidence command or responsibility boundary is inaccurate.

#### 테스트 작성

Do not add tests preemptively. Existing tests cover REST post, redaction, completed-only sink behavior, and scheduler notification fanout. If any focused test fails, add the smallest regression test in the package where the behavior broke.

#### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/adapters/mattermost ./internal/notification ./internal/scheduler
```

Expected: all listed packages pass.

### [MM-2] Server-Generated Push Smoke Evidence

#### 문제

Milestone verification for `mattermost-adapter` requires more than Go unit tests: `agent-roadmap/phase/external-integration/milestones/external-integration.md` requires server-generated signed push evidence, and `agent-test/local/mattermost-server-generated-push-smoke.md` defines FCM receipt, ACK, notification display, opened, channel navigation, inline reply, dismiss/clear, and device token prefix evidence. These depend on private Mattermost/FCM inputs and the standard remote Android runner.

#### 해결 방법

Collect redacted evidence from the standard remote Android runner. Use ignored private input only to execute the smoke; do not copy raw values into the review stub. If the remote runner, sibling paths, emulator, or secrets are missing, fill `사용자 리뷰 요청` with exact blocker evidence and stop.

```bash
# Evidence setup, no secrets printed.
test -f agent-test/local/testing-secrets.md
ssh toki@toki-labs.com 'cd "$HOME/agent-work/nomadcode" && zsh -lc '"'"'git rev-parse --abbrev-ref HEAD && git rev-parse HEAD && test -d ../nexo/packages/messaging_flutter && test -d ../proto-socket/dart && flutter devices'"'"''
```

```bash
# Runtime smoke baseline.
ssh toki@toki-labs.com 'cd "$HOME/agent-work/nomadcode" && zsh -lc '"'"'flutter doctor -v && echo "$ANDROID_HOME" && echo "$ANDROID_SDK_ROOT" && cd apps/client && flutter run -d emulator-5554 --no-resident'"'"''
```

Then run the Mattermost REST user/channel/post steps from `agent-test/local/mattermost-server-generated-push-smoke.md` using a temporary shell script or shell variables outside the repo. The script must check required environment variables with `: "${VAR:?}"`, call `/api/v4/users/me`, `/api/v4/users/email/<receiver-email>`, `/api/v4/channels/direct`, and `/api/v4/posts`, and save only redacted evidence paths/output in the review stub.

#### 수정 파일 및 체크리스트

- [ ] `CODE_REVIEW-cloud-G07.md`: record exact commands and redacted outputs.
- [ ] `/tmp/nomadcode-mm-smoke-evidence.redacted.txt` or another `/tmp` path on the runner: collect redacted evidence; do not add it to the repository unless explicitly scrubbed and needed.
- [ ] `apps/client/assets/mattermost_credentials.json`: create or refresh only as ignored local smoke credential on the remote runner if required.

#### 테스트 작성

No repository test file is expected. This item is an external smoke/evidence task defined by `agent-test/local/mattermost-server-generated-push-smoke.md`.

#### 중간 검증

```bash
ssh toki@toki-labs.com 'cd "$HOME/agent-work/nomadcode" && zsh -lc '"'"'git rev-parse --abbrev-ref HEAD && git rev-parse HEAD && test -d ../nexo/packages/messaging_flutter && test -d ../proto-socket/dart && flutter devices'"'"''
```

Expected: branch and HEAD are printed, sibling path checks pass, and `emulator-5554` or another usable Android device appears.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/adapters/mattermost/client.go` | MM-1, only if regression found |
| `services/core/internal/adapters/mattermost/task_notification_sink.go` | MM-1, only if regression found |
| `services/core/internal/adapters/mattermost/client_test.go` | MM-1, only if coverage gap found |
| `services/core/internal/adapters/mattermost/task_notification_sink_test.go` | MM-1, only if coverage gap found |
| `services/core/internal/scheduler/jobs_test.go` | MM-1, only if notification fanout gap found |
| `services/core/README.md` | MM-1, only if documented command or boundary is inaccurate |
| `CODE_REVIEW-cloud-G07.md` | MM-1, MM-2 evidence and implementation-owned sections |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/adapters/mattermost ./internal/notification ./internal/scheduler
```

Expected: all listed packages pass.

```bash
cd services/core && go test ./...
```

Expected: all core packages pass or report `[no test files]`.

```bash
test -f agent-test/local/testing-secrets.md
```

Expected: exit 0 before external smoke. If missing, record user-owned secret blocker.

```bash
ssh toki@toki-labs.com 'cd "$HOME/agent-work/nomadcode" && zsh -lc '"'"'git rev-parse --abbrev-ref HEAD && git rev-parse HEAD && test -d ../nexo/packages/messaging_flutter && test -d ../proto-socket/dart && flutter doctor -v && echo "$ANDROID_HOME" && echo "$ANDROID_SDK_ROOT" && flutter devices'"'"''
```

Expected: remote checkout, sibling paths, Flutter/Android SDK, and device/emulator are available.

```bash
ssh toki@toki-labs.com 'cd "$HOME/agent-work/nomadcode" && zsh -lc '"'"'cd apps/client && flutter analyze --no-fatal-infos && flutter test'"'"''
```

Expected: analyze completes with no fatal infos; Flutter tests pass.

```bash
ssh toki@toki-labs.com 'cd "$HOME/agent-work/nomadcode" && zsh -lc '"'"'cd apps/client && flutter run -d emulator-5554 --no-resident'"'"''
```

Expected: app launches far enough to initialize Firebase/Mattermost host wiring. If device id differs, record the replacement command and reason.

```bash
git diff --check
```

Expected: no whitespace errors.

For the Mattermost REST positive push and evidence checklist, execute the exact REST/API/logcat/dumpsys commands used by the implementer and paste redacted stdout/stderr into `CODE_REVIEW-cloud-G07.md`. Pass condition: FCM payload wakes, valid signature path, ACK request, notification display, opened event, channel navigation, inline reply, dismiss/clear, and device token prefix are `PASS` or a justified `N/A` for negative control only.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
