<!-- task=m-control-plane-operator-actions/02+01_execution_actions plan=0 tag=EXEC_ACTIONS -->

# Plan - EXEC_ACTIONS

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

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

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

## 배경

`execution-actions`는 실행 취소, report, log, artifact write 진입점을 기존 runner/execution endpoint에 연결한다. 이 작업은 `01_job_create`에서 만든 write result/client/action state 패턴을 재사용해야 하므로 split dependency가 있다. Core route와 proto DTO는 이미 존재하므로 UI는 action entry state와 refresh behavior에 집중한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰가 필요한 경우는 선택된 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:
  - `execution-actions`: 기존 execution/runner write endpoint를 사용하는 취소, report/log/artifact action 진입 상태를 연결한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/control-plane-product-surface/control-plane-operator-actions/SDD.md`
- Acceptance scenarios:
  - `S03`: task=`execution-actions`; evidence=`cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, cancel/report/log/artifact action state tests, `services/core/internal/httpserver/server_test.go` existing route tests
  - `S04`: task=`execution-actions`; evidence=UI refresh test proving write success/deferred triggers read adapter reload without optimistic terminal state
- 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/execution_handlers.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 ./...`는 필수 명령이 아니다. 기존 route behavior는 handler/proto source와 existing server tests를 참조한다.
- `<확인 필요>` 값은 없었다.

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

- 기존 앱/콘솔 tests는 execution read/expand/log/artifact render만 검증한다.
- cancel/report/log/artifact write adapter request body, path/body mismatch error, accepted/success/error mapping tests가 없다.
- write success 후 `_refreshCoreStatus`와 expanded execution detail reload가 실행되어 stale optimistic state가 남지 않는 app-level test가 없다.

### 심볼 참조

- renamed/removed symbol: none.
- 이 plan은 `01_job_create`가 추가한 write result/client/action state symbol을 확장한다. 구현 시작 전 `01_job_create/complete.log`와 실제 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`. 구현은 `agent-task/m-control-plane-operator-actions/01_job_create/complete.log` 또는 matching archive `complete.log`가 생긴 뒤 시작한다.
  - `03+01_runner_actions`: predecessor index `01`.
- predecessor status for this plan: missing at plan creation. runtime dependency는 디렉터리명 `02+01_execution_actions`가 source of truth다.

### 범위 결정 근거

- `services/core/internal/httpserver/**`, `proto/oto/runner.proto`는 변경하지 않는다. 기존 endpoints는 `POST /api/v1/runners/{runnerID}/executions/{executionID}/cancel|report|logs|artifacts`와 `POST /api/v1/executions/{executionID}/logs|artifacts`다.
- 이 plan은 UI action 진입 상태와 adapter mapping만 다룬다. auth/audit/durable policy와 production runner orchestration은 제외한다.
- runner self-update/status action은 sibling `03+01_runner_actions` 범위다.
- 새 package는 추가하지 않는다.

### 빌드 등급

- 결정: `local-G07`.
- 근거: bounded Flutter work지만 cancel/report/log/artifact 네 action과 refresh semantics가 결합되어 blast radius와 test matrix가 `job-create`보다 크다.

## 구현 체크리스트

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

### [EXEC_ACTIONS-1] Extend Write Adapter For Execution Actions

문제: `apps/client/lib/src/app/core_connection_client.dart:147-177`는 read client뿐이고, 현재 write adapter가 없다. Core는 `runner_cicd_handlers.go`에서 cancel/report/log/artifact write 결과를 `success`, `accepted`, `error_message`로 반환하므로 job create와 다른 result parsing이 필요하다.

해결 방법: `01_job_create`에서 만든 `OtoCoreWriteClient`에 execution action methods를 추가한다. Cancel은 `CancelRunRequest`, report는 `ExecutionReportRequest`, log는 `LogAppendRequest`, artifact는 `ArtifactReportRequest` JSON field를 따른다. HTTP 2xx라도 `accepted=false`, `success=false`, `error_message`가 있으면 failed state로 변환한다.

Before:

```dart
// apps/client/lib/src/app/core_connection_client.dart:147-177
abstract class OtoCoreReadClient {
  Future<OtoCoreReadResult<OtoRunnerRecord>> fetchRunner(...);
  ...
  Future<OtoCoreReadResult<List<OtoArtifactRecord>>> fetchArtifacts(...);
}
```

After:

```dart
abstract class OtoCoreWriteClient {
  Future<OtoCoreWriteResult<void>> cancelExecution(...);
  Future<OtoCoreWriteResult<void>> reportExecution(...);
  Future<OtoCoreWriteResult<void>> appendExecutionLog(...);
  Future<OtoCoreWriteResult<void>> appendExecutionArtifact(...);
}
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/core_connection_client.dart`: execution action draft/request models 추가.
- [ ] `apps/client/lib/src/app/core_connection_client.dart`: runner-scoped path/body id를 모두 보내고 mismatch error를 failed state로 변환.
- [ ] `apps/client/test/widget_test.dart`: accepted/success/error/status code mapping tests 추가.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoHttpCoreWriteClient maps execution action responses`
- assertion goal: cancel/report/log/artifact path, JSON body, accepted/success, `error_message`, 400/404/timeout mapping.
- fixture: `MockClient` with runner id `runner-1`, execution id `exec-1`, job id `job-1`.

중간 검증:

```bash
cd apps/client && flutter test --plain-name "OtoHttpCoreWriteClient maps execution action responses"
```

예상 결과: execution write adapter tests가 통과한다.

### [EXEC_ACTIONS-2] Add Execution Action Entry State To Console Surface

문제: `packages/flutter/oto_console/lib/src/oto_executions_surface.dart:4-22`는 expand/read callbacks만 받고, `170-240`의 execution row에는 action controls가 없다. `OtoExecutionViewModel.runnerID`는 있지만 action availability/feedback state가 없다.

해결 방법: console contract에 execution action state map 또는 typed `OtoExecutionActionState`를 추가한다. `OtoExecutionsSurface`는 expanded row 안에 cancel/report/log/artifact compact controls를 렌더링하고, runnerID/executionID가 없거나 terminal state이면 destructive action을 disable한다. Cancel은 confirmation을 요구하고, report/log/artifact는 명확한 entry state와 inline error/success를 표시한다.

Before:

```dart
// packages/flutter/oto_console/lib/src/oto_executions_surface.dart:4-22
class OtoExecutionsSurface extends StatelessWidget {
  final OtoSurfaceSnapshot<List<OtoExecutionViewModel>> snapshot;
  final String? expandedExecutionID;
  ...
  final ValueChanged<String?>? onExpandExecution;
}
```

After:

```dart
class OtoExecutionsSurface extends StatelessWidget {
  final Map<String, OtoExecutionActionState> actionStates;
  final ValueChanged<OtoExecutionCancelDraft>? onCancelExecution;
  final ValueChanged<OtoExecutionReportDraft>? onReportExecution;
  final ValueChanged<OtoExecutionLogDraft>? onAppendLog;
  final ValueChanged<OtoExecutionArtifactDraft>? onAppendArtifact;
  ...
}
```

수정 파일 및 체크리스트:

- [ ] `packages/flutter/oto_console/lib/src/oto_console_contract.dart`: execution action drafts/state 추가.
- [ ] `packages/flutter/oto_console/lib/src/oto_executions_surface.dart`: expanded row에 action controls 추가.
- [ ] `packages/flutter/oto_console/test/oto_console_test.dart`: disabled, confirmation, submitting, success/error tests 추가.

테스트 작성:

- 작성: `packages/flutter/oto_console/test/oto_console_test.dart`
- 테스트명: `OtoExecutionsSurface renders execution action states`
- assertion goal: runnerID 없는 action disabled, cancel confirmation, submitting disables controls, success/error text visible, callbacks receive correct ids.
- fixture: `OtoExecutionViewModel(executionID: 'exec-1', runnerID: 'runner-1', ...)`.

중간 검증:

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

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

### [EXEC_ACTIONS-3] Wire Execution Actions Through OtoClientApp Refresh

문제: `apps/client/lib/src/app/oto_client_app.dart:116-177`의 refresh flow는 read-only이고, `542-553`의 `OtoExecutionsSurface` 연결은 expand/artifacts navigation만 전달한다. write success 이후 jobs/executions/logs/artifacts snapshot을 reload하는 경로가 없다.

해결 방법: app state에 execution action state map을 추가하고, 각 action submit method가 write client를 호출하게 한다. success 또는 deferred에 해당하는 결과는 `_refreshCoreStatus()`를 호출하고 expanded execution이면 `_loadExecutionDetail(execID)`를 다시 호출한다. 실패는 snapshot을 임의로 바꾸지 않고 action state만 failed로 표시한다.

Before:

```dart
// apps/client/lib/src/app/oto_client_app.dart:542-553
executions: OtoExecutionsSurface(
  snapshot: _executionsSnapshot,
  expandedExecutionID: _expandedExecutionID,
  logsSnapshot: _logsSnapshot,
  artifactsSnapshot: _expandedArtifactsSnapshot,
  onExpandExecution: _loadExecutionDetail,
  onViewArtifactsSection: () { ... },
  themeAdapter: themeAdapter,
),
```

After:

```dart
executions: OtoExecutionsSurface(
  snapshot: _executionsSnapshot,
  actionStates: _executionActionStates,
  onCancelExecution: _submitCancelExecution,
  onReportExecution: _submitReportExecution,
  onAppendLog: _submitAppendExecutionLog,
  onAppendArtifact: _submitAppendExecutionArtifact,
  ...
),
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/oto_client_app.dart`: action state map and submit methods 추가.
- [ ] `apps/client/lib/src/app/oto_client_app.dart`: `OtoExecutionsSurface` callbacks 연결.
- [ ] `apps/client/test/widget_test.dart`: fake write client methods와 refresh invocation assertions 추가.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoClientApp wires execution actions and refresh`
- assertion goal: action callback calls write client, success triggers fetchExecution/fetchLogs/fetchArtifacts refresh, failed action displays error and does not mutate read snapshot.
- fixture: `_FakeCoreWriteClient`, `_FakeCoreReadClient`.

중간 검증:

```bash
cd apps/client && flutter test --plain-name "OtoClientApp wires execution actions and refresh"
```

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

## 의존 관계 및 구현 순서

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/src/app/core_connection_client.dart` | EXEC_ACTIONS-1 |
| `apps/client/lib/src/app/oto_client_app.dart` | EXEC_ACTIONS-3 |
| `apps/client/test/widget_test.dart` | EXEC_ACTIONS-1, EXEC_ACTIONS-3 |
| `packages/flutter/oto_console/lib/src/oto_console_contract.dart` | EXEC_ACTIONS-2 |
| `packages/flutter/oto_console/lib/src/oto_executions_surface.dart` | EXEC_ACTIONS-2 |
| `packages/flutter/oto_console/test/oto_console_test.dart` | EXEC_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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
