<!-- task=m-control-plane-operator-actions/01_job_create plan=0 tag=JOB_CREATE -->

# Plan - JOB_CREATE

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

구현의 마지막 단계는 반드시 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가 닫을 수 있는 문제는 사용자 리뷰 요청이 아니라 `검증 결과` 또는 `계획 대비 변경 사항`에 기록한다.

## 배경

현재 Milestone은 read-only Control Plane 위에 기존 Core write endpoint를 연결하는 단계다. `job-create`는 남은 ops-actions 작업 중 공통 write adapter와 action state 패턴을 처음 세우는 작업이므로, 이후 execution/runner action plan의 선행 조건이 된다. Core에는 이미 `POST /api/v1/jobs`가 있으므로 새 Core API/schema/proto 없이 Flutter adapter와 콘솔 action surface만 추가한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰가 필요한 경우는 선택된 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:
  - `job-create`: 기존 `POST /api/v1/jobs` 계약을 사용하는 job 생성 action adapter와 UI 상태를 연결한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/control-plane-product-surface/control-plane-operator-actions/SDD.md`
- Acceptance scenarios:
  - `S02`: task=`job-create`; evidence=`cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, job create adapter/widget tests, related `server_test.go` route coverage 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/job_handlers.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`는 없었다. `agent-test/local/rules.md`의 Monorepo Targets가 `apps/client`와 `packages/flutter/oto_console` 명령을 직접 제공하므로 `create-test`는 수행하지 않는다.
- 적용 명령: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`.
- 구현 범위가 Core API 추가를 금지하므로 `cd services/core && go test ./...`는 최종 필수 명령에서 제외한다. Core route는 기존 source of truth로만 참조한다.
- `<확인 필요>` 값은 없었다. 로컬 규칙의 기본 local 환경은 원격 runner지만, plan의 검증 명령은 repo layout 기준으로 재실행 가능한 패키지 명령이다.

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

- `OtoHttpCoreReadClient` read mapping은 `apps/client/test/widget_test.dart`에서 다루지만, `POST /api/v1/jobs` write adapter, 201/400/409/timeout mapping은 없다.
- `OtoJobsSurface`는 loading/empty/error/data와 execution tap만 검증하며, job create form, disabled/submitting/success/error 상태는 없다.
- `OtoClientApp`은 read refresh만 검증하며, job create 후 returned job id를 read surface refresh 대상으로 합치는 테스트가 없다.

### 심볼 참조

- renamed/removed symbol: none.
- 새로 추가될 symbol 후보: `OtoCoreWriteState`, `OtoCoreWriteResult`, `OtoCoreWriteClient`, `OtoHttpCoreWriteClient`, `OtoJobCreateDraft`, `OtoJobActionState`. 기존 call site는 변경 대신 optional constructor parameter와 callback 추가로 호환성을 유지한다.

### 분할 판단

- split decision policy를 plan file 선택 전에 평가했다.
- shared task group: `agent-task/m-control-plane-operator-actions/`.
- sibling plans:
  - `01_job_create`: 선행 없음. 공통 write adapter/result 패턴과 job create UI를 만든다.
  - `02+01_execution_actions`: predecessor `01_job_create`. `01_job_create/complete.log`가 아직 없으므로 구현은 01 PASS 이후 시작해야 한다.
  - `03+01_runner_actions`: predecessor `01_job_create`. `01_job_create/complete.log`가 아직 없으므로 구현은 01 PASS 이후 시작해야 한다.
- 이 plan은 foundation과 첫 call site가 결합된 단위다. adapter만 분리하면 검증할 사용자 action state가 부족하고, job create UI까지 포함해야 S02 evidence가 닫힌다.

### 범위 결정 근거

- `services/core/internal/httpserver/**`, `proto/oto/runner.proto`는 변경하지 않는다. SDD가 신규 Core API/schema/proto를 범위 제외로 고정했고, `POST /api/v1/jobs`는 이미 존재한다.
- auth, 권한, 감사 로그, durable persistence UI, runner production 운영 정책은 Milestone 범위 제외다.
- execution/runner write action은 sibling plan에서 처리한다. 이 plan은 공통 write result/client가 필요하면 확장하되 completion target은 `job-create`와 S02에 한정한다.
- 새 package는 추가하지 않는다. `apps/client`에는 이미 `http`가 있고, `oto_console`은 transport-free contract/UI만 유지한다.

### 빌드 등급

- 결정: `local-G06`.
- 근거: bounded Flutter adapter/UI/test 작업이며 기존 Core 계약과 deterministic widget/unit tests로 검증 가능하지만, 공통 write contract와 app refresh state를 새로 세우므로 중간 수준 이상의 call-site 영향이 있다.

## 구현 체크리스트

- [ ] `apps/client`에 Core job create write adapter와 result mapping을 추가하고 `cd apps/client && flutter test --plain-name "OtoHttpCoreWriteClient maps job create responses"`가 통과하게 한다.
- [ ] `packages/flutter/oto_console`에 job create action view contract와 UI 상태를 추가하고 `cd packages/flutter/oto_console && flutter test --plain-name "OtoJobsSurface renders job create action states"`가 통과하게 한다.
- [ ] `OtoClientApp`에서 job create action을 연결하고 성공 후 read surface refresh를 트리거하며 `cd apps/client && flutter test --plain-name "OtoClientApp wires job create action and refresh"`가 통과하게 한다.
- [ ] `cd apps/client && flutter test`와 `cd packages/flutter/oto_console && flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [JOB_CREATE-1] Add Core Job Create Write Adapter

문제: `apps/client/lib/src/app/core_connection_client.dart:100-177`에는 read state/result/client만 있고 write action 결과가 없다. `_getObject`는 `GET` 전용이며 `POST /api/v1/jobs`의 201/400/409/timeout을 action state로 표현할 수 없다.

해결 방법: read result와 유사한 write result 타입을 추가하고, `OtoCoreWriteClient.createJob`과 `OtoHttpCoreWriteClient`를 같은 파일에 둔다. 요청 body는 Core handler 기준 `id`, `name`, optional `run_request`만 보낸다. 성공은 HTTP 201 + job DTO, 400/409/invalid JSON/timeout은 user-visible error로 변환한다.

Before:

```dart
// apps/client/lib/src/app/core_connection_client.dart:100-177
enum OtoCoreReadState { loading, empty, data, error }

class OtoCoreReadResult<T> {
  ...
}

abstract class OtoCoreReadClient {
  Future<OtoCoreReadResult<OtoRunnerRecord>> fetchRunner(...);
  ...
}
```

After:

```dart
enum OtoCoreWriteState { idle, submitting, succeeded, deferred, failed }

class OtoCoreWriteResult<T> {
  final OtoCoreWriteState state;
  final T? data;
  final String message;
  final int? statusCode;
  ...
}

abstract class OtoCoreWriteClient {
  Future<OtoCoreWriteResult<OtoJobRecord>> createJob(
    OtoConsoleConfig config,
    OtoJobCreateDraft draft,
  );
}

class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
  @override
  Future<OtoCoreWriteResult<OtoJobRecord>> createJob(...) async {
    final response = await _httpClient
        .post(
          _endpointUri(config.serverHttpUrl, '/api/v1/jobs'),
          headers: const {'content-type': 'application/json'},
          body: jsonEncode(draft.toJson()),
        )
        .timeout(timeout);
    ...
  }
}
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/core_connection_client.dart`: write enum/result/client와 `OtoHttpCoreWriteClient.createJob` 추가.
- [ ] `apps/client/lib/src/app/core_connection_client.dart`: `_endpointUri`를 재사용하고 URL validation error를 write error로 변환.
- [ ] `apps/client/test/widget_test.dart`: `MockClient`로 201/400/409/timeout/invalid config mapping 테스트 추가.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoHttpCoreWriteClient maps job create responses`
- assertion goal: `/api/v1/jobs` POST path, JSON body, 201 data, 400/409 error message/statusCode, invalid base URL error를 검증한다.
- fixture: `MockClient`, `OtoConsoleConfig(serverHttpUrl: 'http://core.example.test:18020', ...)`, created job JSON.

중간 검증:

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

예상 결과: 새 adapter test가 통과한다.

### [JOB_CREATE-2] Add Console Job Create Action Contract And UI

문제: `packages/flutter/oto_console/lib/src/oto_console_contract.dart:136-199`는 generic surface snapshot과 job view model만 제공하며 action view state가 없다. `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart:4-14`는 execution tap callback만 받고, `57-71`의 content switch는 create action UI를 렌더링하지 않는다.

해결 방법: `oto_console_contract.dart`에 transport-free `OtoJobCreateDraft`, `OtoActionStatus`, `OtoActionViewState` 같은 작은 contract를 추가한다. `OtoJobsSurface`는 optional `jobCreateState`와 `onCreateJob` callback을 받고, header 아래에 compact form을 렌더링한다. id/name이 비어 있거나 submitting이면 submit을 disable하고, success/error message를 inline으로 표시한다.

Before:

```dart
// packages/flutter/oto_console/lib/src/oto_jobs_surface.dart:4-14
class OtoJobsSurface extends StatelessWidget {
  final OtoSurfaceSnapshot<List<OtoJobViewModel>> snapshot;
  final OtoConsoleThemeAdapter? themeAdapter;
  final ValueChanged<String>? onExecutionTap;

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

After:

```dart
class OtoJobsSurface extends StatelessWidget {
  final OtoSurfaceSnapshot<List<OtoJobViewModel>> snapshot;
  final OtoActionViewState? jobCreateState;
  final ValueChanged<OtoJobCreateDraft>? onCreateJob;
  ...
}
```

수정 파일 및 체크리스트:

- [ ] `packages/flutter/oto_console/lib/src/oto_console_contract.dart`: action status/state와 job create draft 추가.
- [ ] `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`: form/controller state를 가진 내부 widget 또는 stateful surface로 변경.
- [ ] `packages/flutter/oto_console/test/oto_console_test.dart`: disabled/submitting/success/error callback tests 추가.
- [ ] `packages/flutter/oto_console/lib/oto_console.dart`: 새 contract가 existing export를 통해 노출되는지 확인. 별도 export 추가가 필요하면 최소 변경.

테스트 작성:

- 작성: `packages/flutter/oto_console/test/oto_console_test.dart`
- 테스트명: `OtoJobsSurface renders job create action states`
- assertion goal: empty id/name submit disabled, valid id/name callback payload, submitting disables button, error/success text visible.
- fixture: `OtoJobsSurface(snapshot: OtoSurfaceSnapshot.empty(), onCreateJob: ...)`.

중간 검증:

```bash
cd packages/flutter/oto_console && flutter test --plain-name "OtoJobsSurface renders job create action states"
```

예상 결과: job create surface widget test가 통과한다.

### [JOB_CREATE-3] Wire Job Create Through OtoClientApp And Refresh

문제: `apps/client/lib/src/app/oto_client_app.dart:31-70`에는 `readClient`만 있고 write client injection point가 없다. `_refreshCoreStatus`는 `widget.jobIDs`만 조회하므로 `POST /api/v1/jobs` 성공 후 생성된 job id가 refresh 대상에 포함되지 않는다.

해결 방법: `OtoClientApp`에 optional `writeClient`와 default `OtoHttpCoreWriteClient`를 추가한다. `_createdJobIDs`를 local state로 유지하고 `_fetchJobs`는 `widget.jobIDs + _createdJobIDs`를 dedupe해 조회한다. `_submitJobCreate`는 single in-flight guard를 적용하고, 성공 시 returned job id를 `_createdJobIDs`에 추가한 뒤 `_refreshCoreStatus`를 호출한다. 실패는 optimistic state 없이 `OtoActionViewState.failed`로 표시한다.

Before:

```dart
// apps/client/lib/src/app/oto_client_app.dart:31-70
class OtoClientApp extends StatefulWidget {
  final OtoCoreConnectionClient? coreClient;
  final OtoCoreReadClient? readClient;
  ...
}

class _OtoClientAppState extends State<OtoClientApp> {
  late final OtoCoreConnectionClient _defaultCoreClient =
      OtoHttpCoreConnectionClient();
  late final OtoCoreReadClient _defaultReadClient = OtoHttpCoreReadClient();
```

After:

```dart
class OtoClientApp extends StatefulWidget {
  final OtoCoreWriteClient? writeClient;
  ...
}

class _OtoClientAppState extends State<OtoClientApp> {
  late final OtoCoreWriteClient _defaultWriteClient = OtoHttpCoreWriteClient();
  final Set<String> _createdJobIDs = <String>{};
  OtoActionViewState _jobCreateState = const OtoActionViewState.idle();
  ...
}
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/oto_client_app.dart`: `writeClient` injection, `_createdJobIDs`, `_jobCreateState`, `_submitJobCreate` 추가.
- [ ] `apps/client/lib/src/app/oto_client_app.dart`: `OtoJobsSurface`에 `jobCreateState`와 `onCreateJob` 연결.
- [ ] `apps/client/test/widget_test.dart`: fake write client 추가, success/error/submitting refresh tests 추가.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoClientApp wires job create action and refresh`
- assertion goal: create action calls fake write client, success returned id is fetched via read client, error text is visible, duplicate submit while submitting does not call write twice.
- fixture: `_FakeCoreWriteClient`, `_FakeCoreReadClient` with `onFetchJob`.

중간 검증:

```bash
cd apps/client && flutter test --plain-name "OtoClientApp wires job create action and refresh"
```

예상 결과: app-level job create wiring test가 통과한다.

## 의존 관계 및 구현 순서

1. `JOB_CREATE-1`에서 write adapter/result를 먼저 만든다.
2. `JOB_CREATE-2`에서 console contract/UI를 추가한다.
3. `JOB_CREATE-3`에서 app wiring과 refresh behavior를 연결한다.

## 수정 파일 요약

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