<!-- task=m-external-integration/03_mattermost_adapter plan=1 tag=REVIEW_MM -->

# Plan - REVIEW_MM

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

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

## 배경

첫 리뷰(`code_review_cloud_G07_0.log`)는 `FAIL`이다. Core Mattermost adapter, notification sink, scheduler fanout은 로컬 Go 검증으로 통과했지만, 필수 MM-2 server-generated Mattermost push smoke evidence가 수집되지 않았다. 또한 리뷰 시점에는 로컬 `agent-test/local/testing-secrets.md`가 존재해, 이전 구현 기록의 "로컬 secret 부재" blocker는 현재 상태와 맞지 않는다. 이 후속 계획은 소스 범위를 늘리지 않고 MM-2 evidence를 복구하거나, 현재 시점의 정확한 user-owned blocker로 `USER_REVIEW` 정당성을 다시 세우는 데 한정한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사항은 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-task/m-external-integration/03_mattermost_adapter/plan_cloud_G07_0.log`
- `agent-task/m-external-integration/03_mattermost_adapter/code_review_cloud_G07_0.log`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/mattermost-server-generated-push-smoke.md`
- `agent-ops/rules/project/domain/core/rules.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/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/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `.gitignore`

### 리뷰 판정 근거

- `cd services/core && go test -count=1 ./internal/adapters/mattermost ./internal/notification ./internal/scheduler` 재실행 결과 PASS.
- `cd services/core && go test ./...` 재실행 결과 PASS.
- `git diff --check` 재실행 결과 clean.
- `stat -c '%n %s bytes mode=%a' agent-test/local/testing-secrets.md` 결과 로컬 ignored secret file이 존재한다. 원문 값은 읽거나 기록하지 않는다.
- `git check-ignore -v agent-test/local/testing-secrets.md` 결과 `.gitignore`의 `agent-test/local/` rule로 ignore된다.
- 원격 runner 기존 checkout은 `main` / `55e0139`이고, 로컬 `origin/main`은 `bb43efc`다. 기존 checkout에는 dirty/untracked 파일도 있으므로 직접 덮어쓰지 않는다.
- 원격 runner에는 `apps/client/assets/mattermost_credentials.json`이 존재하고 `emulator-5554`가 보인다. 원격 `agent-test/local/testing-secrets.md`는 없다.

### 범위 결정 근거

- Core adapter source는 이미 검증 PASS이므로 새 코드 변경은 기본 범위가 아니다.
- 기존 원격 runner checkout의 dirty 파일은 사용자/환경 소유 변경일 수 있으므로 수정, reset, checkout, 삭제하지 않는다.
- Android smoke parity는 원격의 fresh checkout 또는 동등하게 안전한 별도 checkout으로 확보한다.
- raw Mattermost token, password, FCM token, signing key, push payload signature는 tracked 파일, task log, 최종 응답에 기록하지 않는다.
- local `testing-secrets.md`는 필요한 값을 사용하는 private input이며, 출력에는 key label과 PASS/BLOCKED 여부만 남긴다.

## 구현 체크리스트

- [ ] [REVIEW_MM-1] 현재 private input blocker를 원문 secret 없이 재검증하고, user-review gate가 필요한지 다시 판정한다.
- [ ] [REVIEW_MM-2] 기존 dirty 원격 checkout을 건드리지 않고 현재 코드 기준 Android runner parity를 확보하거나 정확한 blocker를 기록한다.
- [ ] [REVIEW_MM-3] `agent-test/local/mattermost-server-generated-push-smoke.md` 기준 server-generated Mattermost push evidence를 수집하거나 현재 시점의 정확한 사용자 소유 차단 사유를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_MM-1] Private Input Blocker 재검증

#### 문제

`code_review_cloud_G07_0.log`에는 로컬 `agent-test/local/testing-secrets.md`가 없다고 기록되어 있지만, 리뷰 시점에는 해당 파일이 존재한다. 이 상태에서 곧장 `USER_REVIEW.md`로 멈추면 후속 에이전트가 실행 가능한 검증 증거 공백을 사용자 결정 문제로 잘못 분류하게 된다.

#### 해결 방법

secret 원문을 출력하지 않고 현재 private input의 존재와 필요한 key label 충족 여부만 확인한다. 파일 내용이 필요한 경우 local-only private input으로 읽되, 출력에는 `PRESENT`, `MISSING:<key-label>`, `IGNORED` 같은 redacted 상태만 남긴다. 필요한 key가 실제로 없으면 그 key label과 실행한 명령을 `사용자 리뷰 요청`에 남긴다.

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

- [ ] `CODE_REVIEW-cloud-G07.md`: 로컬 secret 존재/ignore 상태와 required key label check 결과를 redacted output으로 기록한다.
- [ ] `agent-test/local/testing-secrets.md`: 읽기는 허용되지만 원문 값을 출력하거나 tracked 파일에 복사하지 않는다.

#### 테스트 작성

새 repository test는 필요 없다. 이 항목은 검증 evidence recovery다.

#### 중간 검증

```bash
test -f agent-test/local/testing-secrets.md
stat -c '%n %s bytes mode=%a' agent-test/local/testing-secrets.md
git check-ignore -v agent-test/local/testing-secrets.md
```

Expected: secret file exists, mode/size만 출력되고, gitignore rule이 확인된다. 추가 key check는 원문 값을 출력하지 않는 redacted command로 수행한다.

### [REVIEW_MM-2] Remote Runner Parity 확보

#### 문제

기존 원격 runner checkout은 `55e0139`이고 로컬 `origin/main`은 `bb43efc`다. 기존 checkout에는 dirty/untracked 파일도 있어 직접 pull/reset하면 사용자 소유 변경을 건드릴 위험이 있다. 이 상태의 remote analyze/test/run output은 현재 코드의 push smoke 완료 근거로 신뢰하기 어렵다.

#### 해결 방법

기존 `$HOME/agent-work/nomadcode` checkout은 상태 확인만 하고 수정하지 않는다. 필요하면 `$HOME/agent-work/nomadcode-smoke-<timestamp>`처럼 새 디렉터리에 `origin/main`을 clone해 fresh smoke workspace를 만든다. remote ignored credential asset은 기존 runner에 있는 `apps/client/assets/mattermost_credentials.json`을 새 checkout의 같은 경로로 복사하거나, local secret에서 다시 생성한다. 복사/생성 과정에서 credential 원문을 출력하지 않는다.

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

- [ ] `CODE_REVIEW-cloud-G07.md`: remote 기존 checkout 상태, fresh checkout branch/HEAD/status, sibling path, Flutter device 결과를 기록한다.
- [ ] remote fresh checkout: 새 smoke workspace를 만들 수 있으며 tracked repo 파일로 남기지 않는다.
- [ ] remote `apps/client/assets/mattermost_credentials.json`: ignored credential asset으로만 사용하고 원문을 출력하지 않는다.

#### 테스트 작성

새 repository test는 필요 없다. 원격 runtime smoke를 위한 환경 준비다.

#### 중간 검증

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

Expected: 기존 checkout 상태가 기록된다. 이 checkout이 현재 코드와 dirty-free parity가 아니면 수정하지 말고 fresh checkout을 사용한다.

```bash
ssh toki@toki-labs.com 'zsh -lc '"'"'SMOKE_DIR="$HOME/agent-work/nomadcode-smoke-$(date +%Y%m%d%H%M%S)"; echo "SMOKE_DIR=$SMOKE_DIR"; git clone --branch main --single-branch https://git.toki-labs.com/toki/nomadcode.git "$SMOKE_DIR"; cd "$SMOKE_DIR"; git rev-parse --abbrev-ref HEAD; git rev-parse HEAD; git status --short; test -d ../nexo/packages/messaging_flutter; test -d ../proto-socket/dart; if test -f "$HOME/agent-work/nomadcode/apps/client/assets/mattermost_credentials.json"; then cp "$HOME/agent-work/nomadcode/apps/client/assets/mattermost_credentials.json" apps/client/assets/mattermost_credentials.json; fi; test -f apps/client/assets/mattermost_credentials.json; flutter devices'"'"''
```

Expected: fresh checkout HEAD equals `origin/main`, status is clean before ignored credential copy, sibling paths exist, credential asset exists, and `emulator-5554` or a usable Android device is listed. Record the printed `SMOKE_DIR=...` path and substitute that exact path in later remote commands. If clone/copy/device fails, record exact non-secret stdout/stderr as blocker.

### [REVIEW_MM-3] Server-Generated Mattermost Push Evidence

#### 문제

MM-2의 pass condition은 Go unit tests가 아니라 Mattermost REST `POST /api/v4/posts`가 만든 server-generated signed push를 통해 FCM receipt, signature handling, ACK, notification display, opened/channel navigation, inline reply, dismiss/clear, device token prefix evidence를 남기는 것이다. 이전 review stub은 이 전체 checklist를 `BLOCKED`로 남겼다.

#### 해결 방법

fresh/synced remote checkout에서 app을 실행하고, local ignored secret에서 필요한 Mattermost REST 값을 읽어 server-generated DM 또는 mention을 발행한다. REST positive path는 placeholder signature를 쓰지 않는다. Evidence는 redacted logcat, sanitized UI text, `dumpsys notification` excerpt, redacted REST status만 기록한다. 필요한 secret key가 없거나 remote app login/signing-key/FCM registration이 실패하면 정확한 key label 또는 외부 prerequisite을 `사용자 리뷰 요청`에 남긴다.

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

- [ ] `CODE_REVIEW-cloud-G07.md`: `flutter run`, REST trigger, logcat/dumpsys, tap/reply/dismiss evidence를 redacted output으로 기록한다.
- [ ] `/tmp/nomadcode-mm-smoke-evidence.redacted.txt` 또는 remote `/tmp` evidence path: raw secret/signature 없는 evidence만 임시 저장한다.
- [ ] no tracked source changes unless smoke exposes a current-code regression that cannot be explained by environment or credential setup.

#### 테스트 작성

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

#### 중간 검증

```bash
ssh toki@toki-labs.com 'cd "<SMOKE_DIR_FROM_PREVIOUS_OUTPUT>" && zsh -lc '"'"'cd apps/client && flutter analyze --no-fatal-infos && flutter test'"'"''
```

Expected: analyze has no fatal infos and Flutter tests pass.

```bash
ssh toki@toki-labs.com 'cd "<SMOKE_DIR_FROM_PREVIOUS_OUTPUT>" && 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 replacement command and reason.

```bash
# Run from local with values loaded from ignored private input into temporary shell variables.
: "${MATTERMOST_BASE_URL:?}"
: "${MATTERMOST_BOT_TOKEN:?}"
: "${MATTERMOST_RECEIVER_EMAIL:?}"
```

Expected: required env variables are present in the temporary shell. Do not print their values.

Then execute the REST flow from `agent-test/local/mattermost-server-generated-push-smoke.md`: `GET /api/v4/users/me`, `GET /api/v4/users/email/<receiver-email>`, `POST /api/v4/channels/direct`, and `POST /api/v4/posts`, using `Authorization: Bearer <bot-access-token-from-local-test-secrets>`. Save only redacted status, ids prefixes, and evidence checklist results.

## 최종 검증

```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
git diff --check
```

Expected: no whitespace errors.

```bash
test -f agent-test/local/testing-secrets.md
git check-ignore -v agent-test/local/testing-secrets.md
```

Expected: local secret exists and is ignored; no raw secret values are printed.

```bash
ssh toki@toki-labs.com 'cd "<SMOKE_DIR_FROM_PREVIOUS_OUTPUT>" && zsh -lc '"'"'git rev-parse --abbrev-ref HEAD && git rev-parse HEAD && git status --short && test -d ../nexo/packages/messaging_flutter && test -d ../proto-socket/dart && flutter devices'"'"''
```

Expected: fresh/synced checkout is current for smoke, sibling paths exist, and Android device/emulator is available.

```bash
ssh toki@toki-labs.com 'cd "<SMOKE_DIR_FROM_PREVIOUS_OUTPUT>" && zsh -lc '"'"'cd apps/client && flutter analyze --no-fatal-infos && flutter test && flutter run -d emulator-5554 --no-resident'"'"''
```

Expected: host wiring verifies with no fatal analyze issue, tests pass, and app launches.

For the Mattermost REST positive push and evidence checklist, execute the exact REST/API/logcat/dumpsys/tap/reply/dismiss 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
