<!-- task=m-proto-socket-infrastructure-communication-rail/03+02_workspace_integration plan=1 tag=REVIEW_API -->

# Plan - REVIEW_API Workspace Integration Follow-Up

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

이 plan의 구현이 끝나면 active `CODE_REVIEW-*-G??.md`에서 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채워야 한다. 최종 archive, `complete.log`, 리뷰 판정은 code-review-skill 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret 준비, 또는 안전한 진행을 막는 scope 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정과 근거를 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 메울 수 있는 검증 공백만으로는 사용자 리뷰 요청을 만들지 않는다.

## 배경

첫 리뷰에서 workspace integration 구현은 proto-socket task service를 UI에 연결했지만, 실제 앱의 async 연결 handoff와 lifecycle ownership edge case가 누락됐다. 또한 API-1을 검증할 app-level widget test가 없어 기본 선택 workspace가 service data를 받는지 확인하지 못했다. 이 follow-up은 새 기능을 넓히지 않고, 리뷰에서 발견된 Required 이슈만 닫는다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/mobile/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/mobile-smoke.md`
- `agent-task/m-proto-socket-infrastructure-communication-rail/03+02_workspace_integration/plan_local_G05_0.log`
- `agent-task/m-proto-socket-infrastructure-communication-rail/03+02_workspace_integration/code_review_local_G05_0.log`
- `apps/client/lib/src/app/bootstrap.dart`
- `apps/client/lib/src/app/nomadcode_client_app.dart`
- `apps/client/lib/src/features/workspaces/domain/project_workspace.dart`
- `apps/client/lib/src/features/workspaces/domain/workspace_task.dart`
- `apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart`
- `apps/client/lib/src/integrations/proto_socket/proto_socket_endpoint_config.dart`
- `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart`
- `apps/client/lib/src/integrations/proto_socket/proto_socket_task_service.dart`
- `apps/client/test/widget_test.dart`
- `apps/client/test/integrations/proto_socket_lifecycle_test.dart`
- `apps/client/test/integrations/proto_socket_task_service_test.dart`
- `apps/client/pubspec.yaml`
- `apps/client/pubspec.lock`

### 테스트 환경 규칙

- Chosen `test_env`: `local`.
- `agent-test/local/rules.md` was present and read. It states current local test/verification execution is prohibited, so do not run Flutter commands on the local host unless that rule is changed by the user/runtime.
- Matched profile: `agent-test/local/mobile-smoke.md`, present and read. It lists `cd apps/client && flutter test` and `cd apps/client && flutter analyze --no-fatal-infos` as mobile baseline commands.
- Verification commands below are the required mobile commands for a permitted remote/mobile-capable runner. If only the current local host is available, record them as BLOCKED with the rule citation instead of inventing substitute evidence.

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

- Delayed `loadTasks` injection after the parent app connects proto-socket is not covered. Existing tests pass `loadTasks` at initial widget construction.
- `NomadCodeClientApp` does not have tests for `protoSocketConfig == null`, `enabled == false`, `enabled == true`, already-connected lifecycle ownership, or dispose while connect is in flight.
- Lockfile churn has no associated dependency change or verification rationale.

### 심볼 참조

- `WorkspaceHomePage`: call sites are `apps/client/lib/src/app/nomadcode_client_app.dart` and `apps/client/test/widget_test.dart`.
- `WorkspaceTaskLoader`: defined only in `workspace_home_page.dart`; no external rename required.
- `ProtoSocketLifecycle.connect` / `disconnect`: used by `nomadcode_client_app.dart` and integration tests.

### 분할 판단

This is a follow-up inside existing split subtask `03+02_workspace_integration`; do not create another sibling subtask. The changes are tightly coupled: app lifecycle ownership, page handoff, and widget tests must be reviewed together to verify the same async path. The original predecessor gate `02+01_task_service/complete.log` was recorded as user-approved bypass in `code_review_local_G05_0.log`; this follow-up must not synthesize or edit predecessor completion artifacts.

### 범위 결정 근거

Only touch the current workspace integration surface and its tests. Do not change Core Go code, proto-socket envelope/service parsing, task channel action names, Mattermost/Firebase integration, platform runner files, or roadmap documents. Do not introduce new packages.

### 빌드 등급

Build lane `cloud-G05`, review lane `cloud-G05`: first local review found correctness and test coverage failures across app lifecycle, service handoff, and lockfile scope. The work is still small, but needs stronger API/call-site review than the original local pass.

## 구현 체크리스트

- [ ] [REVIEW_API-1] Reload workspace tasks when `WorkspaceHomePage.loadTasks` changes after initial build.
- [ ] [REVIEW_API-2] Fix proto-socket lifecycle ownership so the app disconnects only connections it actually started and cleans up successful late connects after dispose.
- [ ] [REVIEW_API-3] Add app-level widget tests for enabled/disabled proto-socket config, delayed service handoff, and lifecycle dispose ownership.
- [ ] [REVIEW_API-4] Remove or justify the unplanned `apps/client/pubspec.lock` transitive dependency changes.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_API-1] Delayed Task Loader Handoff

### 문제

`apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart:28-34` calls `_reloadTasks()` only during `initState`. In the real app, `apps/client/lib/src/app/nomadcode_client_app.dart:112-138` first renders with `loadTasks == null` and later rebuilds with a service-backed loader after async connect. Because `WorkspaceHomePage` has no `didUpdateWidget`, `_tasksFuture` stays null and `_buildTaskSection` renders the empty state instead of service tasks.

### 해결 방법

Add an update hook that refreshes the current selection when the loader availability changes. Keep mock fallback when the loader becomes null.

Example direction:

```dart
@override
void didUpdateWidget(covariant WorkspaceHomePage oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.loadTasks == widget.loadTasks) return;
  _reloadTasks();
}
```

If the parent still creates a fresh closure on every build, either make the parent loader stable or compare the null/non-null transition so ordinary parent rebuilds do not trigger unnecessary reload loops.

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

- [ ] Update `apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart`.
- [ ] Preserve existing mock rows when `loadTasks == null`.
- [ ] Preserve reload on project selection and retry.
- [ ] Do not redesign the page.

### 테스트 작성

Add a widget test in `apps/client/test/widget_test.dart` that pumps `WorkspaceHomePage` first with `loadTasks: null`, then rebuilds the same state with a non-null loader, and asserts task titles/statuses render for the default selected project.

### 중간 검증

```bash
cd apps/client && flutter test test/widget_test.dart --plain-name "reloads tasks when loader is attached after initial build"
```

Expected: passes on a permitted runner. If only the current local host is available, record BLOCKED because `agent-test/local/rules.md` prohibits local test execution.

## [REVIEW_API-2] Lifecycle Ownership And Dispose Race

### 문제

`apps/client/lib/src/app/nomadcode_client_app.dart:109-126` treats any successful `connect()` return as a widget-owned connection. `ProtoSocketLifecycle.connect()` is a no-op when already connected, so the app can disconnect an externally owned lifecycle. A second edge case exists when the widget disposes while `connect()` is in flight: the await may succeed after dispose, then return at `!mounted` without disconnecting the connection this widget started.

### 해결 방법

Track ownership from lifecycle state before connecting, not just from successful return. If the lifecycle was already connected or connecting, use the available transport without marking it widget-owned. If this widget started the connect and it completes after dispose, call `disconnect()` before returning. In `dispose`, fire the disconnect only for widget-owned connections.

Example direction:

```dart
final startedHere =
    lifecycle.state != ProtoSocketConnectionState.connected &&
    lifecycle.state != ProtoSocketConnectionState.connecting;
await lifecycle.connect(config);
final transport = lifecycle.transport;
if (transport == null) return;
if (!mounted) {
  if (startedHere) await lifecycle.disconnect();
  return;
}
setState(() {
  _taskService = ProtoSocketTaskService(transport);
  _connectedByThisWidget = startedHere;
});
```

Use `unawaited(...)` or another explicit pattern for async cleanup in `dispose` if needed.

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

- [ ] Update `apps/client/lib/src/app/nomadcode_client_app.dart`.
- [ ] Do not connect when lifecycle/config is null or config is disabled.
- [ ] Do not disconnect an already-connected externally owned lifecycle.
- [ ] Disconnect a successful late connect if the widget was disposed before the await resumed.
- [ ] Keep connect failure fallback non-fatal.

### 테스트 작성

Covered by REVIEW_API-3 app-level widget tests.

### 중간 검증

```bash
cd apps/client && flutter test test/widget_test.dart --plain-name "NomadCodeClientApp"
```

Expected: the new app lifecycle tests pass on a permitted runner. If only the current local host is available, record BLOCKED because `agent-test/local/rules.md` prohibits local test execution.

## [REVIEW_API-3] App-Level Widget Tests

### 문제

`apps/client/test/widget_test.dart:45-107` covers direct `WorkspaceHomePage(loadTasks: ...)` states, but not the API-1 app wiring path. This allowed the delayed handoff and ownership bugs to pass.

### 해결 방법

Add fake `ProtoSocketConnector` and `ProtoSocketTransport` fixtures to `widget_test.dart` or a small test helper inside the same file. The fake transport should return a `task.list` response with one deterministic `WorkspaceTask`. Exercise `NomadCodeClientApp` with injected `ProtoSocketLifecycle` and `ProtoSocketEndpointConfig`.

Required tests:

- `protoSocketConfig == null` or `enabled == false` does not call connector.
- `enabled == true` connects once and renders service task data after the async connection completes.
- Already-connected lifecycle renders service data but app dispose does not call connector `disconnect()`.
- Dispose while app-started connect is in flight cleans up after the connect future completes.

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

- [ ] Update `apps/client/test/widget_test.dart`.
- [ ] Reuse existing proto-socket domain models; do not open sockets.
- [ ] Assert non-secret error behavior remains covered.
- [ ] Keep existing launcher callback test green.

### 테스트 작성

This item is the test implementation for REVIEW_API-1 and REVIEW_API-2.

### 중간 검증

```bash
cd apps/client && flutter test test/widget_test.dart
```

Expected: all widget tests pass on a permitted runner. If only the current local host is available, record BLOCKED because `agent-test/local/rules.md` prohibits local test execution.

## [REVIEW_API-4] Lockfile Scope Cleanup

### 문제

`apps/client/pubspec.lock:211-218` and `apps/client/pubspec.lock:302-309` downgrade transitive `meta` and `test_api`, but `apps/client/pubspec.yaml` did not change and this plan does not add dependencies. That is unplanned dependency churn in a UI integration task.

### 해결 방법

If the lockfile changes are accidental, restore `apps/client/pubspec.lock` to the prior tracked versions for `meta` and `test_api`. If they are necessary for the runner used by this task, keep them only with a clear `계획 대비 변경 사항` note explaining the exact command and why the lockfile regeneration belongs to this task.

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

- [ ] Review `apps/client/pubspec.lock`.
- [ ] Remove accidental `meta` / `test_api` downgrade churn, or document the necessary reason in the active review stub.
- [ ] Do not edit `pubspec.yaml` unless a real dependency change is required by REVIEW_API-1 through REVIEW_API-3.

### 테스트 작성

No separate test. Lockfile cleanup is verified by `git diff -- apps/client/pubspec.lock` and the final Flutter commands.

### 중간 검증

```bash
git diff -- apps/client/pubspec.lock
```

Expected: no diff if the churn was accidental, or a documented minimal diff if necessary.

## 의존 관계 및 구현 순서

Implement REVIEW_API-1 and REVIEW_API-2 before REVIEW_API-3 so the app-level tests can assert fixed behavior. REVIEW_API-4 can be done last. Do not create `complete.log` for `02+01_task_service`; predecessor completion is outside this follow-up scope.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart` | REVIEW_API-1 |
| `apps/client/lib/src/app/nomadcode_client_app.dart` | REVIEW_API-2 |
| `apps/client/test/widget_test.dart` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-3 |
| `apps/client/pubspec.lock` | REVIEW_API-4 |

## 최종 검증

```bash
git diff -- apps/client/pubspec.lock
cd apps/client && flutter test test/widget_test.dart
cd apps/client && flutter test
cd apps/client && flutter analyze --no-fatal-infos
```

Expected: lockfile diff is absent or explicitly justified; widget tests and full Flutter tests pass; analyzer has no fatal issues. These commands must run only on a permitted runner. If only the current local host is available, record BLOCKED with `agent-test/local/rules.md` because local test execution is currently prohibited.

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