<!-- task=02+01_catalog_cli plan=1 tag=REVIEW_CATALOG_CLI -->

# Catalog CLI Follow-up Plan

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

이 계획은 `code_review_cloud_G07_0.log`의 FAIL Required 항목만 해결한다. 범위를 넓히지 말고, 실제 bin stdout 계약을 검증하는 테스트와 스모크를 반드시 포함한다. 구현 마지막에는 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 출력으로 채운다.

## 배경

첫 리뷰에서 `oto catalog --json`의 실제 bin stdout이 JSON 파서에서 실패하고, `oto catalog` 기본 명령이 테이블 대신 help를 출력하는 문제가 확인되었다. 이 작업은 catalog CLI 노출 계약을 회복하는 좁은 follow-up이다.

## 의존 관계 및 구현 순서

- 기준 리뷰: `agent-task/02+01_catalog_cli/code_review_cloud_G07_0.log`
- 구현 순서: raw JSON 출력 보존 → no-arg catalog 실행 허용 → bin-level 테스트/검증 갱신 → 리뷰 파일 작성.

## 분석 결과

### 읽어야 할 파일

- `agent-ops/rules/project/domain/cli/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `lib/cli/cli.dart`
- `lib/cli/printer.dart`
- `lib/cli/commands/command_base.dart`
- `lib/cli/commands/command_manager.dart`
- `lib/cli/commands/command_catalog.dart`
- `bin/main.dart`
- `test/oto_catalog_cli_test.dart`
- `README.md`
- `agent-ops/roadmap/milestones/structured-automation-surface.md`

### 실패 근거

- `dart run bin/main.dart catalog --json | jq .`가 `Invalid numeric literal`로 실패한다. `CommandCatalogCli`의 기본 출력 콜백이 `CLI.println`을 사용하고, Unix printer가 `echo -e "..."`로 감싸면서 JSON quote를 제거한다.
- `dart run bin/main.dart catalog`는 `CommandManager`의 빈 인자 help 경로를 타서 catalog table이 아니라 Usage/help를 출력한다.
- 기존 테스트는 `printString` 주입 경로만 JSON decode하므로 실제 CLI printer 경로 손상을 잡지 못한다.

### 범위 결정

- JSON schema 필드 확장, command catalog contract 확장, YAML validation/result/event 작업은 하지 않는다.
- 기존 command들의 인자 없는 실행 동작은 유지한다.
- printer 전체 리팩터링은 피하고, catalog CLI 출력 계약을 보존하는 최소 변경을 우선한다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`. 실제 bin stdout, shell/CLI 출력, parseability가 성공 조건이라 terminal-agent work에 해당한다.

## 구현 체크리스트

- [x] [REVIEW_CATALOG_CLI-1] 실제 bin JSON stdout을 parseable JSON으로 보존한다.
- [x] [REVIEW_CATALOG_CLI-2] `oto catalog` no-arg 기본 테이블 출력을 동작시킨다.
- [x] 중간 검증과 최종 검증을 모두 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_CATALOG_CLI-1] 실제 bin JSON stdout을 parseable JSON으로 보존한다

### 문제

`CommandCatalogCli`는 주입 테스트에서는 JSON 문자열을 그대로 수집하지만, 프로덕션 기본 콜백은 `CLI.println`이다.

Before: [command_catalog.dart](/config/workspace/oto/lib/cli/commands/command_catalog.dart:9)

```dart
CommandCatalogCli({Future<void> Function(String)? printString})
    : _printString = printString ?? ((val) => CLI.println(val));
```

`CLI.println`은 개행을 제거하고, Unix printer는 shell script의 `echo -e "..."` 안에 문자열을 넣는다. 그 결과 실제 stdout은 `"schemaVersion"`이 아니라 `schemaVersion`처럼 quote가 제거되어 JSON이 아니다.

### 해결 방법

catalog CLI의 기본 출력은 JSON과 table 모두 raw stdout을 보존하는 경로를 사용한다. 가장 좁은 후보는 `dart:io`의 `stdout.writeln(value)`를 기본 `printString`으로 사용하는 것이다. 더 넓은 `CLI.printRaw` API를 추가해도 되지만, 기존 command 출력 동작을 바꾸지 않도록 호출 범위를 catalog CLI로 제한한다.

After 후보:

```dart
import 'dart:io';

CommandCatalogCli({Future<void> Function(String)? printString})
    : _printString = printString ??
          ((value) async {
            stdout.writeln(value);
          });
```

테스트는 실제 bin 실행을 `Process.run` 또는 동등한 방식으로 호출해 `jsonDecode(stdout)`가 성공하고 `Execute command:`와 ANSI escape가 없는지 확인한다.

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

- [x] `lib/cli/commands/command_catalog.dart`에서 기본 출력 콜백이 JSON quote/newline을 보존하도록 수정
- [x] `test/oto_catalog_cli_test.dart`에 실제 bin `catalog --json` parse 테스트 추가
- [x] 테스트에서 stdout에 `Execute command:`가 없고 JSON decode 결과의 `type`이 `commandCatalog`인지 확인
- [x] 기존 주입 프린터 테스트는 유지

### 테스트 작성

작성한다. 실제 bin-level JSON parseability를 회귀 테스트로 고정해야 한다.

### 중간 검증

```bash
dart test test/oto_catalog_cli_test.dart
dart run bin/main.dart catalog --json | jq -e '.type == "commandCatalog" and .schemaVersion == 1'
```

예상 결과: 테스트가 통과하고 `jq` 명령이 exit code 0으로 종료한다.

## [REVIEW_CATALOG_CLI-2] `oto catalog` no-arg 기본 테이블 출력을 동작시킨다

### 문제

계획과 README는 `oto catalog`를 사람용 table 출력으로 안내하지만, `CommandManager`는 인자가 비어 있으면 command를 실행하지 않고 help를 출력한다.

Before: [command_manager.dart](/config/workspace/oto/lib/cli/commands/command_manager.dart:37)

```dart
if (!printHelp) {
  if (parameters.isEmpty) {
    commandItem.printHelp();
  } else {
    ...
    await commandItem.execute(parameters);
  }
}
```

### 해결 방법

기존 command의 no-arg help 동작은 유지하면서 catalog만 빈 인자 실행을 허용하는 hook을 추가한다. 예시는 아래와 같다.

```dart
abstract class CommandBase {
  bool shouldExecuteWithoutParameters() => false;
}
```

`CommandManager`는 `parameters.isEmpty && !commandItem.shouldExecuteWithoutParameters()`일 때만 help를 출력하고, `CommandCatalogCli`는 true를 반환한다. `oto catalog` 실행 시 불필요한 API 확장이나 command별 예외 처리가 섞이지 않도록 변경을 좁힌다.

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

- [x] `lib/cli/commands/command_base.dart`에 기존 command 기본값이 false인 no-arg 실행 hook 추가
- [x] `lib/cli/commands/command_manager.dart`에서 빈 인자 처리 시 hook 확인
- [x] `lib/cli/commands/command_catalog.dart`에서 no-arg 실행 hook true override
- [x] `test/oto_catalog_cli_test.dart`에 실제 bin `catalog` table smoke 추가
- [x] smoke에서 `Command`, `Category`, `DataModel`, `SamplePath`, `SampleStatus` header가 있고 `Usage:` help가 아닌지 확인

### 테스트 작성

작성한다. 직접 class 호출 테스트만으로는 `CommandManager`의 no-arg 분기 회귀를 잡을 수 없다.

### 중간 검증

```bash
dart test test/oto_catalog_cli_test.dart
output="$(dart run bin/main.dart catalog)"; printf '%s\n' "$output" | rg 'Command +\\| Category +\\| DataModel +\\| SamplePath +\\| SampleStatus'; ! printf '%s\n' "$output" | rg 'Usage:'
```

예상 결과: 테스트가 통과하고 table header가 검색되며 `Usage:`는 검색되지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/cli/commands/command_catalog.dart` | REVIEW_CATALOG_CLI-1, REVIEW_CATALOG_CLI-2 |
| `lib/cli/commands/command_base.dart` | REVIEW_CATALOG_CLI-2 |
| `lib/cli/commands/command_manager.dart` | REVIEW_CATALOG_CLI-2 |
| `test/oto_catalog_cli_test.dart` | REVIEW_CATALOG_CLI-1, REVIEW_CATALOG_CLI-2 |

## 최종 검증

```bash
test -f agent-task/01_command_catalog_contract/complete.log
dart test test/oto_command_catalog_test.dart test/oto_catalog_cli_test.dart
dart analyze
dart run bin/main.dart catalog --json | jq -e '.type == "commandCatalog" and .schemaVersion == 1'
output="$(dart run bin/main.dart catalog)"; printf '%s\n' "$output" | rg 'Command +\\| Category +\\| DataModel +\\| SamplePath +\\| SampleStatus'; ! printf '%s\n' "$output" | rg 'Usage:'
rg --sort path -n "oto catalog|command catalog를 CLI" README.md agent-ops/roadmap/milestones/structured-automation-surface.md
```

예상 결과: 모든 명령이 0으로 종료한다. JSON smoke는 실제 JSON parser를 통과하고, no-arg catalog는 help가 아닌 table을 출력한다.
