<!-- task=execution_result_envelope/02+01_exe_json_cli plan=0 tag=EXE_JSON -->

# Exe JSON CLI Plan

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

구현 완료 전 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제 실행하고, 실제 출력과 구현 메모를 남긴 뒤 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 아카이브와 `complete.log` 작성은 code-review 스킬 전용이다.

## 배경

`BuildResult` envelope가 생기면 기존 `oto exe` 실행 결과를 외부 자동화가 파싱할 수 있는 CLI 출력으로 연결해야 한다. 현재 `CommandExe`는 exit code만 isolate complete 이벤트로 받고, `--json` 옵션이 없으며, `-f`를 만나면 즉시 실행하고 나머지 인자를 무시한다. 이 작업은 `oto exe -f <file> --json`을 안정적인 JSON stdout과 exit code 계약으로 만든다.

## 분석 결과

### 읽은 파일

- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/structured-automation-surface.md`
- `agent-ops/rules/project/domain/cli/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `lib/cli/commands/command_exe.dart`
- `lib/cli/commands/command_base.dart`
- `lib/cli/commands/command_manager.dart`
- `lib/cli/commands/command_validate.dart`
- `lib/cli/commands/command_catalog.dart`
- `lib/cli/cli.dart`
- `lib/cli/commands/scheduler/scheduler_isolate.dart`
- `lib/oto/core/build_result.dart`
- `lib/oto/application.dart`
- `bin/main.dart`
- `test/oto_application_test.dart`
- `test/oto_validate_cli_test.dart`
- `test/oto_catalog_cli_test.dart`
- `test/oto_scheduler_runtime_test.dart`
- `pubspec.yaml`

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

- `CommandExe.shouldPrintExecuteLog(['--json'])`: 기존 테스트 없음.
- `CommandExe` direct JSON output: 기존 테스트 없음.
- actual bin `dart run bin/main.dart exe -f <file> --json`: 기존 테스트 없음.
- missing file JSON envelope와 exit code: 기존 테스트는 missing file이 LateInitializationError 없이 끝나는지만 확인한다.

### 심볼 참조

- renamed/removed symbols: none.
- `CommandExe` 참조: `bin/main.dart`, `lib/cli/commands/scheduler/scheduler_isolate.dart`, `test/oto_application_test.dart`.
- `IsolateExe` complete event data: [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:73)에서 int만 처리하고, [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:138)에서 `result.exitCode`만 보낸다.

### 분할 판단

분할 정책을 먼저 평가했다. 공유 task group은 `execution_result_envelope`이다.

- `01_result_contract`: 선행 작업. `BuildResult.toJson()`과 `logEnable` stdout 억제 기반을 제공해야 한다.
- `02+01_exe_json_cli`: 이 작업. 디렉터리명 기준으로 `01_result_contract`의 `complete.log`가 선행 조건이다.

CLI 인자 파싱, isolate 결과 전달, actual bin smoke는 core 계약과 독립 검증 단위가 다르므로 별도 plan으로 둔다.

### 범위 결정 근거

- 이 작업은 `oto exe -f <file> --json`을 우선 검증 대상으로 한다.
- `-j`, `-t` 기존 실행 경로는 깨지지 않게 유지하되 별도 bin smoke는 추가하지 않는다.
- scheduler 내부 실행은 JSON 출력 대상이 아니므로 `executeScheduler()` 동작을 보존한다.
- step event, log stream, artifact event는 이번 plan 밖이다.
- 새 패키지는 추가하지 않는다. JSON encoding은 `dart:convert`를 사용한다.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`.
- terminal CLI workflow, stdout parsing, exit-status contract, actual bin smoke가 성공 조건이라 `cloud-G07` 이상이 필요하다.

## 구현 체크리스트

- [ ] `01_result_contract`의 `complete.log`가 존재하는지 확인하고 시작한다.
- [ ] `CommandExe` 인자 파싱을 `--json` 위치와 무관하게 동작하도록 정리한다.
- [ ] isolate complete 이벤트가 `BuildResult.toJson()` envelope를 전달하고 기존 scheduler 경로를 보존하도록 처리한다.
- [ ] `oto exe -f <file> --json`이 stdout에 JSON만 출력하고 process exit code를 envelope와 맞추도록 구현한다.
- [ ] `test/oto_application_test.dart`에 CommandExe JSON unit 및 actual bin smoke 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 하위 작업은 `01_result_contract`에 의존한다. 구현 시작 전 `agent-task/execution_result_envelope/01_result_contract/complete.log` 또는 archive된 PASS 결과가 있어야 한다.

### [EXE_JSON-1] CommandExe JSON Mode And Result Transport

#### 문제

[command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:30)의 `execute()`는 인자를 순회하다가 `-j`, `-t`, `-f`를 만나면 즉시 실행 후 `break`한다. 따라서 `exe -f file.yaml --json` 같은 후행 옵션을 해석할 수 없다. 또한 [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:73)는 complete 이벤트 data가 int인 경우만 처리하고, [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:138)는 isolate에서 `result.exitCode`만 보낸다.

Before:

```dart
// lib/cli/commands/command_exe.dart:30
Future execute(List<String> parameters) async {
  for (var i = 0; i < parameters.length; ++i) {
    var item = parameters[i];
    if (item.startsWith('-j')) {
      _handler = IsolateManager.create(IsolateExe(BuildType.jenkins));
      await isComplete();
      break;
    } else if (item.startsWith('-t')) {
      _handler = IsolateManager.create(IsolateExe(BuildType.test));
      await isComplete();
      break;
    } else if (item.startsWith('-f')) {
      var file = '';
      if (parameters.length > i + 1) {
        file = parameters[i + 1];
      } else {
        file = item.replaceFirst('-f', '').trimLeft();
      }
      await startYaml(file);
      break;
    } else {
      printHelp();
    }
  }
  exitCode = _exitCode;
  _handler?.exit();
  return simpleFuture;
}
```

#### 해결 방법

`execute()`를 먼저 파싱하고 나중에 실행하는 구조로 바꾼다. `--json`, `--file`, `--file=<path>`, `-f <path>`, `-fpath`를 지원하고, JSON 모드에서는 `shouldPrintExecuteLog()`가 false를 반환한다.

complete 이벤트는 기존 int와 새 Map envelope를 모두 처리한다.

```dart
_handler!.addListener('complete', (data) {
  if (data is Map) {
    _resultJson = Map<String, dynamic>.from(data);
    _exitCode = (_resultJson!['exitCode'] as num?)?.toInt() ?? 10;
  } else if (data is int) {
    _exitCode = data;
  }
  _complete = true;
});
```

`IsolateExe.onReady()`는 `result.toJson()`을 보낸다. `executeScheduler()`는 JSON을 출력하지 않고 기존처럼 완료만 기다리도록 유지한다.

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

- [ ] `lib/cli/commands/command_exe.dart`: `dart:convert`와 `BuildResult` import 추가.
- [ ] `lib/cli/commands/command_exe.dart`: `_resultJson`과 optional `_printString` 추가.
- [ ] `lib/cli/commands/command_exe.dart`: `arguments`에 `--json`, `--file` 설명 추가.
- [ ] `lib/cli/commands/command_exe.dart`: `shouldPrintExecuteLog()` override 추가.
- [ ] `lib/cli/commands/command_exe.dart`: 인자 파싱을 선파싱/후실행 구조로 변경.
- [ ] `lib/cli/commands/command_exe.dart`: complete listener가 Map envelope와 int를 모두 처리하도록 변경.
- [ ] `lib/cli/commands/command_exe.dart`: `IsolateExe.onReady()`에서 `result.toJson()` 전송.

#### 테스트 작성

- 작성: `test/oto_application_test.dart`
- 테스트명:
  - `CommandExe shouldPrintExecuteLog suppresses execute log for json`
  - `CommandExe exe -f missing file --json returns execution result json`
- 검증 목표: JSON 옵션이 위치와 무관하게 인식되고, missing file에서도 JSON envelope와 exit code 10이 나온다.

#### 중간 검증

```bash
dart test test/oto_application_test.dart --plain-name "CommandExe"
```

기대 결과: 기존 missing file 테스트와 새 CommandExe JSON 테스트가 통과한다.

### [EXE_JSON-2] Actual Bin Pure JSON Smoke

#### 문제

마일스톤 완료 기준은 외부 자동화가 내부 Dart API가 아니라 CLI를 호출해 결과를 해석하는 것이다. [command_manager.dart](/config/workspace/oto/lib/cli/commands/command_manager.dart:41)는 기본적으로 `Execute command:` 로그를 출력하고, [command_validate.dart](/config/workspace/oto/lib/cli/commands/command_validate.dart:32)와 [command_catalog.dart](/config/workspace/oto/lib/cli/commands/command_catalog.dart:32)는 JSON 모드에서 이를 끄는 패턴을 이미 갖고 있다. `exe --json`도 actual bin smoke로 stdout이 parseable JSON인지 검증해야 한다.

Before:

```dart
// lib/cli/commands/command_manager.dart:41
if (commandItem.shouldPrintExecuteLog(parameters)) {
  await CLI.println('Execute command: $command$argStr',
      color: Color.green);
}
```

#### 해결 방법

`CommandExe.shouldPrintExecuteLog()`를 `CommandCatalogCli`/`CommandValidateCli`와 같은 패턴으로 구현한다. JSON 모드 실행은 `Application.build(..., logEnable: false)` 기반으로 돌리고, 완료 후 `JsonEncoder.withIndent('  ')`로 envelope를 한 번만 stdout에 쓴다.

`01_result_contract`에서 `Application.log()`가 `_logEnable`을 존중해야 `PrintCommand`가 있는 YAML도 stdout을 오염시키지 않는다.

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

- [ ] `lib/cli/commands/command_exe.dart`: JSON 모드에서 `IsolateExe`에 `logEnable: false` 전달.
- [ ] `lib/cli/commands/command_exe.dart`: 실행 완료 후 `_resultJson`을 stdout에 출력.
- [ ] `test/oto_application_test.dart`: 실제 `dart run bin/main.dart exe -f <temp.yaml> --json` 테스트 추가.
- [ ] `test/oto_application_test.dart`: stdout에 `Execute command:`, `Usage:`, YAML command output이 섞이지 않는지 확인.

#### 테스트 작성

- 작성: `test/oto_application_test.dart`
- 테스트명: `actual bin exe --json output has parseable execution result json and no execute logs`
- fixture: temp YAML에 `Print` command를 넣어 `Application.log()` 억제까지 검증한다.
- assertion: process exit code 0, stdout JSON parse 성공, `type == executionResult`, `success == true`, `exitCode == 0`, stdout에 `Execute command:`, `Usage:`, YAML message가 없음.

#### 중간 검증

```bash
dart test test/oto_application_test.dart --plain-name "actual bin exe --json"
```

기대 결과: actual bin JSON smoke가 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/cli/commands/command_exe.dart` | EXE_JSON-1, EXE_JSON-2 |
| `test/oto_application_test.dart` | EXE_JSON-1, EXE_JSON-2 |

## 최종 검증

```bash
dart analyze
dart test test/oto_application_test.dart
dart test test/oto_validate_cli_test.dart test/oto_catalog_cli_test.dart test/oto_scheduler_runtime_test.dart
```

기대 결과: analyze issue 없이 통과하고 관련 CLI/application/scheduler 회귀 테스트가 모두 통과한다.

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