<!-- task=m-control-plane-operator-actions/03+01_runner_actions plan=0 tag=RUNNER_ACTIONS -->

# Plan - RUNNER_ACTIONS

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

구현의 마지막 단계는 반드시 active `CODE_REVIEW-local-G06.md`에서 구현 에이전트 소유 섹션을 실제 변경 내용, 설계 결정, 검증 출력으로 채우는 것이다. 검증 명령을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log` 작성, archive 이동은 code-review-skill 전용이다.

선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목 때문에 구현이 막히면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 연결 대상, 증거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/service 차단, 일반 범위 조정, 검증 증거 공백처럼 후속 agent가 닫을 수 있는 문제는 사용자 리뷰 요청이 아니라 `검증 결과` 또는 `계획 대비 변경 사항`에 기록한다.

## 배경

`runner-actions`는 runner status read surface 위에 self-update write action의 accepted/deferred/error 상태를 표시하는 작업이다. Core에는 `GET /api/v1/runners/{id}/status`와 `POST /api/v1/runners/{id}/self-update`가 이미 존재한다. 이 작업도 `01_job_create`의 write action state pattern을 사용하되, execution action과는 독립적으로 진행할 수 있다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰가 필요한 경우는 선택된 SDD 결정 또는 선택된 Milestone lock 결정이 실구현을 차단할 때뿐이다. 구현 에이전트는 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사된 active review stub의 `사용자 리뷰 요청` 섹션만 채우고 멈춘다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-operator-actions.md`
- Task ids:
  - `runner-actions`: 기존 runner self-update/status action 표면을 연결하고 실패/대기/완료 상태를 표시한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/control-plane-product-surface/control-plane-operator-actions/SDD.md`
- Acceptance scenarios:
  - `S05`: task=`runner-actions`; evidence=`cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, self-update accepted/deferred/error tests, `TestHandleRunnerSelfUpdate` reference
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-operator-actions.md`
- `agent-roadmap/sdd/control-plane-product-surface/control-plane-operator-actions/SDD.md`
- `apps/client/pubspec.yaml`
- `apps/client/lib/src/app/core_connection_client.dart`
- `apps/client/lib/src/app/oto_client_app.dart`
- `apps/client/test/widget_test.dart`
- `packages/flutter/oto_console/pubspec.yaml`
- `packages/flutter/oto_console/lib/src/oto_console_contract.dart`
- `packages/flutter/oto_console/lib/src/oto_console_shell.dart`
- `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`
- `packages/flutter/oto_console/lib/src/oto_executions_surface.dart`
- `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`
- `packages/flutter/oto_console/lib/src/oto_artifacts_surface.dart`
- `packages/flutter/oto_console/lib/oto_console.dart`
- `packages/flutter/oto_console/test/oto_console_test.dart`
- `services/core/internal/httpserver/routes.go`
- `services/core/internal/httpserver/runner_cicd_handlers.go`
- `services/core/internal/httpserver/dto.go`
- `proto/oto/runner.proto`

### 테스트 환경 규칙

- 선택한 test_env: `local`.
- `agent-test/local/rules.md`가 존재하며 전체를 읽었다.
- 매칭 profile: `apps/client`, `packages/flutter/oto_console`, `services/core`에 대응하는 `agent-test/local/*project-smoke.md`는 없었다. Monorepo Targets가 필요한 Flutter test 명령을 제공하므로 `create-test`는 수행하지 않는다.
- 적용 명령: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`.
- Core route/proto는 변경하지 않으므로 `cd services/core && go test ./...`는 필수 명령이 아니다.
- `<확인 필요>` 값은 없었다.

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

- 기존 runner tests는 loading/empty/error/data read rendering만 검증한다.
- self-update write adapter의 HTTPS validation error, accepted, deferred, `error_message` mapping tests가 없다.
- runner surface에는 target version/download URL 입력, submitting/deferred/error/success 표시, callback tests가 없다.
- app-level self-update success/deferred 이후 runner status refresh tests가 없다.

### 심볼 참조

- renamed/removed symbol: none.
- 이 plan은 `01_job_create`가 추가한 write result/client/action state symbol을 확장한다. 구현 시작 전 completed symbol 이름을 확인하고 plan과 다르면 `계획 대비 변경 사항`에 기록한다.

### 분할 판단

- split decision policy를 plan file 선택 전에 평가했다.
- shared task group: `agent-task/m-control-plane-operator-actions/`.
- sibling plans:
  - `01_job_create`: 선행 foundation. active dir은 존재하지만 plan 작성 시점에 `complete.log`는 없다.
  - `02+01_execution_actions`: predecessor index `01`.
  - `03+01_runner_actions`: predecessor index `01`. 구현은 `agent-task/m-control-plane-operator-actions/01_job_create/complete.log` 또는 matching archive `complete.log`가 생긴 뒤 시작한다.
- predecessor status for this plan: missing at plan creation. runtime dependency는 디렉터리명 `03+01_runner_actions`가 source of truth다.

### 범위 결정 근거

- `services/core/internal/httpserver/**`, `proto/oto/runner.proto`는 변경하지 않는다. self-update endpoint와 status read endpoint가 이미 존재한다.
- 이 plan은 runner self-update/status action surface만 다룬다. runner enrollment, bootstrap, auth/audit, private release host/secret 관리는 제외한다.
- execution cancel/report/log/artifact action은 sibling `02+01_execution_actions` 범위다.
- 새 package는 추가하지 않는다.

### 빌드 등급

- 결정: `local-G06`.
- 근거: bounded Flutter adapter/UI/test 작업이며 accepted/deferred/error 상태 구분이 핵심이다. `job-create` foundation을 재사용하므로 execution actions보다 scope가 작다.

## 구현 체크리스트

- [ ] `01_job_create` predecessor `complete.log`를 확인한 뒤 해당 write adapter/result 패턴을 확장하고 `cd apps/client && flutter test --plain-name "OtoHttpCoreWriteClient maps runner self-update responses"`가 통과하게 한다.
- [ ] `packages/flutter/oto_console`에 runner self-update/status action surface를 추가하고 `cd packages/flutter/oto_console && flutter test --plain-name "OtoRunnersSurface renders runner action states"`가 통과하게 한다.
- [ ] `OtoClientApp`에서 runner self-update action success/deferred 이후 runner status refresh를 연결하고 `cd apps/client && flutter test --plain-name "OtoClientApp wires runner self-update and refresh"`가 통과하게 한다.
- [ ] `cd apps/client && flutter test`와 `cd packages/flutter/oto_console && flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [RUNNER_ACTIONS-1] Extend Write Adapter For Runner Self Update

문제: `apps/client/lib/src/app/core_connection_client.dart:147-177`에는 runner status read method만 있고 self-update write method가 없다. Core `handleRunnerSelfUpdate`는 `success`, `accepted`, `deferred`, `message`, `error_message`, `target_version`을 반환하므로 deferred를 success와 구분해야 한다.

해결 방법: `01_job_create`의 `OtoCoreWriteClient`에 `selfUpdateRunner` method와 `OtoRunnerSelfUpdateDraft`를 추가한다. body는 `runner_id`, `version`, `download_url`만 사용한다. HTTP 200 + `deferred=true`는 `OtoCoreWriteState.deferred`, `accepted=true`는 succeeded, `success=false` 또는 `error_message`는 failed로 변환한다.

Before:

```dart
// apps/client/lib/src/app/core_connection_client.dart:153-156
Future<OtoCoreReadResult<OtoRunnerStatus>> fetchRunnerStatus(
  OtoConsoleConfig config,
  String runnerID,
);
```

After:

```dart
abstract class OtoCoreWriteClient {
  Future<OtoCoreWriteResult<OtoRunnerSelfUpdateResult>> selfUpdateRunner(
    OtoConsoleConfig config,
    OtoRunnerSelfUpdateDraft draft,
  );
}
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/core_connection_client.dart`: self-update draft/result model과 write method 추가.
- [ ] `apps/client/lib/src/app/core_connection_client.dart`: `deferred=true`를 별도 state로 변환.
- [ ] `apps/client/test/widget_test.dart`: accepted/deferred/error/invalid HTTPS mapping tests 추가.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoHttpCoreWriteClient maps runner self-update responses`
- assertion goal: `/api/v1/runners/runner-1/self-update` path/body, accepted/deferred/error state, target version/message mapping.
- fixture: `MockClient`, version `v1.2.3`, HTTPS download URL placeholder `https://releases.example.test/oto-agent`.

중간 검증:

```bash
cd apps/client && flutter test --plain-name "OtoHttpCoreWriteClient maps runner self-update responses"
```

예상 결과: self-update adapter tests가 통과한다.

### [RUNNER_ACTIONS-2] Add Runner Action Surface State

문제: `packages/flutter/oto_console/lib/src/oto_runners_surface.dart:4-12`는 snapshot만 받으며, `160-235`의 runner row는 status/current job 정보만 표시한다. self-update target version/download URL 입력과 accepted/deferred/error feedback이 없다.

해결 방법: console contract에 `OtoRunnerSelfUpdateDraft`와 runner action state를 추가한다. `OtoRunnersSurface`는 optional action state map과 `onSelfUpdateRunner` callback을 받아 runner row에 compact self-update action을 렌더링한다. download URL이 HTTPS가 아니면 submit을 disable하거나 inline validation error를 표시하고, submitting/deferred/succeeded/failed 상태를 분리한다.

Before:

```dart
// packages/flutter/oto_console/lib/src/oto_runners_surface.dart:4-12
class OtoRunnersSurface extends StatelessWidget {
  final OtoSurfaceSnapshot<List<OtoRunnerViewModel>> snapshot;
  final OtoConsoleThemeAdapter? themeAdapter;

  const OtoRunnersSurface({
    super.key,
    required this.snapshot,
    this.themeAdapter,
  });
```

After:

```dart
class OtoRunnersSurface extends StatelessWidget {
  final Map<String, OtoRunnerActionState> actionStates;
  final ValueChanged<OtoRunnerSelfUpdateDraft>? onSelfUpdateRunner;
  ...
}
```

수정 파일 및 체크리스트:

- [ ] `packages/flutter/oto_console/lib/src/oto_console_contract.dart`: runner self-update draft/action state 추가.
- [ ] `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`: row-level self-update form과 feedback 추가.
- [ ] `packages/flutter/oto_console/test/oto_console_test.dart`: validation/submitting/deferred/success/error callback tests 추가.

테스트 작성:

- 작성: `packages/flutter/oto_console/test/oto_console_test.dart`
- 테스트명: `OtoRunnersSurface renders runner action states`
- assertion goal: HTTPS URL validation, callback payload, submitting disabled, deferred message, error message.
- fixture: `OtoRunnerViewModel(runnerID: 'runner-1', status: 'idle', ...)`.

중간 검증:

```bash
cd packages/flutter/oto_console && flutter test --plain-name "OtoRunnersSurface renders runner action states"
```

예상 결과: runner action surface tests가 통과한다.

### [RUNNER_ACTIONS-3] Wire Runner Self Update Through OtoClientApp Refresh

문제: `apps/client/lib/src/app/oto_client_app.dart:528-531`의 `OtoRunnersSurface` 연결은 snapshot/theme만 전달한다. self-update success/deferred 이후 runner status를 다시 조회하는 경로가 없다.

해결 방법: app state에 runner action state map과 `_submitRunnerSelfUpdate`를 추가한다. result가 succeeded 또는 deferred면 `_refreshCoreStatus()`를 호출해 `fetchRunner`와 `fetchRunnerStatus`를 다시 실행한다. failed result는 runner snapshot을 임의로 바꾸지 않고 action state만 failed로 표시한다.

Before:

```dart
// apps/client/lib/src/app/oto_client_app.dart:528-531
runners: OtoRunnersSurface(
  snapshot: _runnersSnapshot,
  themeAdapter: themeAdapter,
),
```

After:

```dart
runners: OtoRunnersSurface(
  snapshot: _runnersSnapshot,
  actionStates: _runnerActionStates,
  onSelfUpdateRunner: _submitRunnerSelfUpdate,
  themeAdapter: themeAdapter,
),
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/oto_client_app.dart`: runner action state map and submit method 추가.
- [ ] `apps/client/lib/src/app/oto_client_app.dart`: `OtoRunnersSurface` callbacks 연결.
- [ ] `apps/client/test/widget_test.dart`: fake write client self-update와 refresh assertions 추가.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoClientApp wires runner self-update and refresh`
- assertion goal: self-update callback calls write client, accepted/deferred triggers `fetchRunnerStatus`, failed state displays error and does not mutate runner snapshot.
- fixture: `_FakeCoreWriteClient`, `_FakeCoreReadClient`.

중간 검증:

```bash
cd apps/client && flutter test --plain-name "OtoClientApp wires runner self-update and refresh"
```

예상 결과: app runner action wiring test가 통과한다.

## 의존 관계 및 구현 순서

1. 구현 시작 전에 predecessor `01_job_create`의 `complete.log`를 확인한다. 디렉터리명 `03+01_runner_actions`의 `+01`이 유일한 runtime dependency다.
2. `RUNNER_ACTIONS-1`에서 completed write adapter pattern을 확장한다.
3. `RUNNER_ACTIONS-2`에서 console runner action state를 추가한다.
4. `RUNNER_ACTIONS-3`에서 app wiring과 refresh behavior를 연결한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/src/app/core_connection_client.dart` | RUNNER_ACTIONS-1 |
| `apps/client/lib/src/app/oto_client_app.dart` | RUNNER_ACTIONS-3 |
| `apps/client/test/widget_test.dart` | RUNNER_ACTIONS-1, RUNNER_ACTIONS-3 |
| `packages/flutter/oto_console/lib/src/oto_console_contract.dart` | RUNNER_ACTIONS-2 |
| `packages/flutter/oto_console/lib/src/oto_runners_surface.dart` | RUNNER_ACTIONS-2 |
| `packages/flutter/oto_console/test/oto_console_test.dart` | RUNNER_ACTIONS-2 |

## 최종 검증

```bash
cd apps/client && flutter test
cd packages/flutter/oto_console && flutter test
git diff --check
```

예상 결과: 모든 명령이 exit 0이다. Flutter/Go test cache 여부는 해당 없음이며, widget/unit test는 fresh process로 실행된다.

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