# ✅ Peer Review Report: M1 Dashboard & Detail Pane Implementation (Commit 2eb8586) **Job**: `cb97a36f` · **Reviewer**: Reviewer B (Cline, `canary-projects-multi-agent-mux-reviewer-cline`) **Review Target**: Commit `2eb8586` — "feat(ui): complete M1 Milestone - read-only Dashboard and Detail Pane with status.sh integration" **Plan Reference**: `.agents/reports/canary-projects-multi-agent-mux-planner-reviewer-claude/report-final.md` (Rev.3 — Flutter 전면 재작성 계획서) **Review Scope**: 계획서에 입각하여 제출된 코드가 안전하고 모순 없이 구현되었는지 교차 검증 (구현하지 않음, 리뷰만 수행) **Method**: 계획서 §3(D8), §5(아키텍처), §6(예외처리/보안 계약), §10(DoD)를 실제 커밋 코드와 라인 단위 교차 검증 + 라이브 실행实证 + Dart 테스트/정적 분석 실행 --- ## 1. 커밋 개요 커밋 `2eb8586`는 M1 마일스톤(읽기 전용 대시보드 + Detail Pane)을 구현. 22개 파일, +1941/-326행. 핵심 변경: - `status.sh` additive 스키마 확장 (D8 해법 구현, +103/-5행) - `packages/mam_core/` — 순수 Dart 데이터/서비스 계층 (models, command_runner, session_service, status_repository) - `apps/mam_desktop/` — Flutter Desktop UI (main, session_table, detail_pane, stale_banner, providers, theme, status_script_locator) - `packages/mam_core/test/session_service_test.dart` — 3개 단위 테스트 --- ## 2. D8 — `status.sh --json` additive 스키마 확장 (§3.1) 검증 **계획서 요구**: 기존 5개 키(timestamp/yaml_path/tmux_sessions_alive/tmux_confirmed/drifts/actions) 무변경 + 신규 `sessions_detail` 키 추가. 텍스트 모드 byte-identical 회귀 없음. **라이브 실행实证**: ``` $ bash status.sh --json | python3 -m json.tool top keys: ['timestamp', 'yaml_path', 'tmux_sessions_alive', 'tmux_confirmed', 'drifts', 'actions', 'sessions_detail'] sessions_detail count: 2 sessions_detail[0] keys: ['name', 'server', 'status', 'tmux_alive', 'cmd', 'role', 'resume_state', 'job_id', 'job_status', 'pane_cwd', 'attach_command', 'drift_classes', 'pane_pid', 'cmd_full', 'start_command', 'last_visible_status'] ``` - 기존 6개 키(timestamp/yaml_path/tmux_sessions_alive/tmux_confirmed/drifts/actions) **전부 보존** ✅ - 신규 `sessions_detail` 키 추가 ✅ - `sessions_detail` 필드가 계획서 §3.1의 D8 계약(name/server/status/tmux_alive/cmd/role/resume_state/job_id/job_status/pane_cwd/attach_command/drift_classes)과 **field-for-field 일치** ✅ - additive beyond D8: `pane_pid`/`cmd_full`/`start_command`/`last_visible_status` — Detail Pane용 추가 필드, 계획서가 "세션명/워크스페이스 등을 계산하는 부분"이라 명시한 범위 내 ✅ **텍스트 모드 회귀 검증 (DoD-1)**: ``` $ diff <(old status.sh text output) <(new status.sh text output) 1c1 < agent-sessions status — 2026-07-16T12:16:37Z (tmux_confirmed=True) --- > agent-sessions status — 2026-07-16T12:16:38Z (tmux_confirmed=True) ``` 유일한 차이는 타임스탬프(1초) — 본문 byte-identical ✅. git diff 분석: 변경은 `--json` 분기(조기 exit 제거 + 새 Python 블록 추가)에만 국한, 텍스트 모드 Python 블록(라인 31~119)은 **무변경** ✅. --- ## 3. 아키텍처 준수 (§5) 검증 ### 3.1 모노레포 패키지 구조 (§5.2) **검증**: `packages/mam_core/`(순수 Dart, Flutter 비의존) + `apps/mam_desktop/`(Flutter Desktop) 분리 구현 ✅. `mam_core`가 `dart:io`/`dart:convert`/`package:meta`만 의존하고 Flutter 엔진 의존성이 없음을 확인 — 헤드리스 실행 가능 원칙 준수. `mam_core.dart` barrel export가 models/services/command_runner를 깔끔히 노출. ### 3.2 `command_runner.dart` — 유일한 서브프로세스 실행 지점 (§6.1, D5) **검증**: - `Process.start(argv.first, argv.sublist(1), runInShell: false)` — argv list 강제, `runInShell: false` 명시 ✅ (D5 계약) - `Future.any([exitFuture, Future.delayed(timeout)])`로 클라이언트측 타임아웃 강제 ✅ (§6.1) - `killOnTimeout` 파라미터: `true`면 SIGTERM→5s→SIGKILL, `false`면 프로세스 백그라운드 완주 + `backgroundFuture` 반환 ✅ (D-Critical purge 계약) - `CommandResult`에 `timedOut`/`backgroundFuture` 필드로 타임아웃 상태 명확히 구분 ✅ **평가**: ✅ §6.1 의사코드 계약을 정확히 구현. D5(명령 주입 방지) + D-Critical(purge 원자성 보존) 모두 충족. ### 3.3 `status_repository.dart` — 폴링 + stale/backoff (§6.6, D6) **검증**: - `Stream watch()` — 폴링 루프, 실패 시 `lastGood` 스냅샷 유지 + `stale: true` 표시 ✅ (D6) - 백오프: `failureBackoff = [3s, 6s, 15s]` — 계획서 §6.6 "3s→6s→최대 15s"와 일치 ✅ - `SessionsPoll` 모델: `snapshot`/`stale`/`lastOkAt`/`error` — stale 배너에 필요한 정보 전부 포함 ✅ - 기본 폴링 간격 4초(계획서는 3초 권장) — 경미한 차이이나 계획서가 "기본 3초, 설정 가능"이라 했으므로 구현 재량 범위 내 **평가**: ✅ D6 계약 정확히 구현. UI가 null/blank dashboard를 보지 않도록 보장. ### 3.4 `session_service.dart` — status.sh --json 래핑 (§2, Rev.1 §1) **검증**: - `runCommand(['bash', statusScriptPath, '--json'], timeout: 5s)` — 조회 5초 타임아웃(§6.1) ✅ - `timedOut`/`rc != 0`/`jsonDecode` 실패 시 `StatusFetchException` throw — `StatusRepository`가 이를 catch해 stale 처리 ✅ - `decoded is! Map` 타입 가드 ✅ - "이 코드는 YAML/SQLite/jsonl을 직접 읽지 않는다" — `status.sh --json` 출력만 소비, Rev.1 §1 원칙 준수 ✅ **평가**: ✅ 단일 진실 공급원 원칙 준수. --- ## 4. UI 계층 검증 (§7 화면 설계) ### 4.1 `main.dart` — DashboardScreen (§7 Sessions 대시보드) **검증**: - `ProviderScope` + `ConsumerWidget` — Riverpod 상태관리 (§5.1) ✅ - `sessionsPollProvider` StreamProvider 구독 → `pollAsync.when(data/loading/error)` ✅ - Master-Detail 레이아웃: `SessionTable`(flex:3) + `DetailPane`(width:380) ✅ (§7) - `_ErrorScreen` — 폴링 시작 실패 시 에러 화면 ✅ - `StaleBanner` — stale 상태 표시 ✅ (D6) ### 4.2 `session_table.dart` — DataTable2 (§7) **검증**: - `data_table_2` 사용 (§5.1 스택 선정) ✅ - 컬럼: `NAME/SERVER/YAML/TMUX/CMD/RESUME/JOB_ID/JOB_STATUS/DRIFT` — 계획서 §7 "Rev.1 §4.1과 동일 컬럼 셋" 정확히 일치 ✅ - 행 선택(`onTap` → `onSelect`) → `selectedSessionNameProvider` 업데이트 ✅ - `_StatusChip`/`_TmuxChip` — 상태별 색상 코딩(running=success, dead=danger) ✅ - 빈 상태 처리(`empty:` widget) ✅ ### 4.3 `detail_pane.dart` — Detail Pane (§7) **검증**: - `SessionRow?` null 처리 → `_EmptyDetail`("Select a session") ✅ - PANE 섹션: pid/cwd/cmd/cmd_full ✅ - ATTACH 섹션: attach_command/start_command + 복사 버튼(`Clipboard.setData`) ✅ (§4 "복사 버튼" 요구사항) - STATUS 섹션: last_visible_status/resume_state/job_id/job_status/drift_classes ✅ - `SelectableText` — 텍스트 선택 가능 ✅ - `_Header` — 세션명 + 상태 pill(status/tmux/role/server) ✅ ### 4.4 `stale_banner.dart` — D6 stale 배너 (§6.6) **검증**: - `poll.stale` false → `SizedBox.shrink()` (숨김) ✅ - stale true → 경고 배너 "⚠ status snapshot stale (last ok: HH:MM:SS)" ✅ - `lastOkAt` 포맷팅(HH:MM:SS) ✅ ### 4.5 `status_script_locator.dart` — 스크립트 경로 해석 **검증**: `.git` 마커로 repo root walk-up → 고정 경로 하강. 하드코딩 절대경로 없음. `flutter run` 실행 디렉터리 무관 robustness ✅. 계획서가 명시하지 않았으나 구현 품질 향상(Rev.1 §8 "no hardcoded absolute path" 원칙 계승). ### 4.6 `session_providers.dart` — Riverpod wiring **검증**: `sessionServiceProvider` → `statusRepositoryProvider` → `sessionsPollProvider` 계층적 의존성 주입 ✅. `apps/mam_desktop`이 폴링/백오프 로직을 재구현하지 않고 `mam_core`에 위임 ✅ (§6.6 "Framework agnostic" 원칙). --- ## 5. DoD (§10) 실증 검증 계획서 §10의 12개 DoD 항목 중 M1 범위에서 검증 가능한 항목들을 실제로 실행 검증: | DoD | 항목 | 검증 방법 | 결과 | |-----|------|----------|------| | 1 | `status.sh` 회귀 없음 (텍스트 모드 byte-identical) | old vs new text output diff | ✅ PASS (타임스탬프만 차이, 본문 동일) | | 2 | 비파괴 검증 (mam_core/pty에 파일 쓰기/삭제 없음) | `grep -rn` | ✅ PASS (코드 전무) | | 3 | 명령 주입 방어 (`runInShell: true` 금지) | `grep -rn 'runInShell'` | ✅ PASS (`runInShell: false`만 존재) | | 9 | 정적 분석 (`dart analyze` clean) | `dart analyze` 실행 | ✅ PASS (No issues found!) | | 11 | 회귀 없음 (stop/create/resume/monitor/lib.sh 무변경) | `git diff --stat` | ✅ PASS (status.sh만 변경) | | — | Dart 단위 테스트 | `dart test` 실행 | ✅ PASS (3/3 All tests passed!) | **DoD-1 상세 (jq diff 대체 검증)**: 기존 5개 키(timestamp/yaml_path/tmux_sessions_alive/tmux_confirmed/drifts) + actions 키가 신규 `sessions_detail` 추가 전후로 동일함을 라이브 실행으로 확인. `sessions_detail`은 순수 additive. **테스트 커버리지** (`session_service_test.dart`): 1. `SessionsSnapshot.fromJson` well-formed payload 파싱 — drift 클래스, role, resume_state, pane.pid, attach_command 전부 정확히 매핑 ✅ 2. 누락 필드 허용(`{"name": "bare"}`) — 기본값(`?`/`-`/null) 적용 ✅ 3. 실제 `status.sh --json` 출력 파싱 — 라이브 연동 검증 ✅ **평가**: ✅ M1 범위 DoD 전부 충족. 테스트는 실제 `status.sh` 라이브 연동까지 검증하여 매우 견고함. --- ## 6. 코드 품질 관찰 (NON-BLOCKING — PASS에 영향 없음) 아래 항목들은 통과를 막는 결함이 아니며, 향후 마일스톤에서 고려하면 더 견고해지는 사항이다. 1. **폴링 간격 (선택)**: 계획서 §6.6/§7이 "기본 3초"를 권장했으나 `StatusRepository` 기본값이 4초(`pollInterval: Duration(seconds: 4)`). 경미한 차이이며 계획서가 "설정 가능"이라 명시했으므로 구현 재량 범위. 향후 사용자 피드백에 따라 조정 가능. 2. **`sessions_detail` additive 필드 (주의 권고)**: `pane_pid`/`cmd_full`/`start_command`/`last_visible_status` 4개 필드가 계획서 §3.1의 D8 예시 스키마를 초과해 추가됨. 코드 주석이 "Additive beyond the D8 example — needed by the M1 Detail Pane"이라 명시했으므로 의도적 확장이며, `SessionRow.fromJson`이 이를 안전히 파싱(누락 시 null). 회귀 위험 없음. 단, 향후 `status.sh` 출력 스키마를 문서화할 때 이 4개 필드도 계획서에 갱신하면 추적성 향상. 3. **`status_script_locator.dart` 예외 메시지 (선택)**: `.git` 디렉터리를 못 찾았을 때 "Run mam_desktop from within the multi-agent-mux repo checkout"이라는 안내가 명확. 다만 submodule/worktree 환경에서 `.git`이 파일인 경우(`.git` 디렉터리가 아님)를 고려하면 더 robust해짐. (현재 환경에서는 이슈 없음) 4. **`_StatusChip` switch 표현식 (선택)**: `case 'stopped': case 'terminated': case 'archived':` fallthrough가 의도한 대로 동작하나, Dart 3 switch 표현식에서 여러 case가 연속일 때 가독성이 약간 떨어질 수 있음. 기능적으로 정확하므로 스타일 선호 영역. --- ## 7. AGENTS.md 원칙 준수 검증 - **Surgical Changes (§3)**: 변경이 M1 대시보드/Detail Pane + D8 `status.sh` 확장에만 국한. 기존 스크립트(stop/create/resume/monitor/lib.sh) 무변경. `git diff --stat`로 확인 ✅ - **Simplicity First (§2)**: `mam_core`(순수 Dart) + `mam_desktop`(Flutter) 관심사 분리. `command_runner.dart` 유일 실행 지점으로 과잉 추상화 없음. 각 모델 클래스 단일 책임 ✅ - **Goal-Driven Execution (§4)**: §10 DoD 항목 전부 관측 가능(grep/diff/dart test/dart analyze). 라이브 실행实证으로 회귀 없음 입증 ✅ - **문서-코드 정합성**: 계획서 §3.1 D8 스키마 ↔ `status.sh` `sessions_detail` 출력 ↔ `SessionRow.fromJson` 매핑 — 3계층 전부 field-for-field 일치 ✅ --- ## 8. 종합 평가 커밋 `2eb8586`는 계획서(Rev.3)의 M1 마일스톤(읽기 전용 대시보드 + Detail Pane)을 충실하게 구현했다. 핵심 성과: 1. **D8 additive 스키마 확장 정확 구현**: `status.sh --json`이 기존 6개 키를 무변경으로 보존하면서 `sessions_detail` 신규 키를 추가. 라이브 실행实证으로 기존 소비자 회귀 없음을 확인했으며, 텍스트 모드는 byte-identical(타임스탬프만 차이). 2. **불변 안전 계약 정확 이식**: `command_runner.dart`가 D5(명령 주입 방지, `runInShell: false`) + D-Critical(purge `killOnTimeout: false` + `backgroundFuture`) + §6.1 타임아웃(조회 5초)을 정확히 구현. `status_repository.dart`가 D6(stale 스냅샷 유지 + 백오프 3s→6s→15s)을 충족. 3. **모노레포 관심사 분리**: `mam_core`(순수 Dart, Flutter 비의존)가 데이터/서비스 계층을 담당하고 `mam_desktop`이 Riverpod으로 wiring — 계획서 §5.2 구조 정확히 반영. 4. **견고한 테스트**: 3개 단위 테스트(파싱 정확성 + 누락 필드 허용 + 실제 `status.sh` 라이브 연동) 전부 통과. `dart analyze` No issues found. 5. **회귀 없음**: 기존 셸 스크립트(stop/create/resume/monitor/lib.sh) 전부 무변경, `status.sh`는 `--json` 분기 내부에만 additive 변경. 개선 권고 4건은 모두 NON-BLOCKING(구현 재량/스타일/향후 문서화)으로 통과 판정에 영향을 주지 않는다. 코드는 계획서에 입각해 안전하고 모순 없이 구현되었다. [VERDICT: PASS]