<!-- task=02+01_catalog_cli plan=0 tag=CATALOG_CLI -->

# Catalog CLI Plan

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

**구현의 마지막 단계로 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채워야 한다. 이 파일을 채우기 전에는 작업이 완료된 것이 아니다.** `구현 체크리스트`를 순서대로 수행하고, 계획과 리뷰 스텁의 체크리스트를 모두 완료한 뒤, 중간/최종 검증을 실행하고 실제 출력으로 `CODE_REVIEW-*-G??.md`를 채운다. 리뷰 파일의 `이 파일을 읽는 리뷰 에이전트에게` 아카이브 지시는 실행하지 말고, `코드리뷰 전용 체크리스트`도 수정하거나 체크하지 않는다.

## 배경

사용자 결정에 따라 command catalog는 사람용 table과 AI용 JSON을 모두 제공해야 한다. JSON stdout은 외부 자동화가 바로 파싱할 수 있어야 하므로 기존 `CommandManager`의 실행 로그가 섞이면 안 된다. 이 작업은 `01_command_catalog_contract`가 만든 command catalog contract를 CLI top-level 명령으로 노출한다.

## 의존 관계 및 구현 순서

- 이 디렉터리 이름은 `02+01_catalog_cli`이므로 `agent-task/01_command_catalog_contract/complete.log`가 생긴 뒤 구현한다.
- 구현 순서: CLI 로그 억제 hook 추가 → catalog CLI 추가 → top-level 등록 → 테스트/문서/로드맵 갱신.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/cli/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `agent-ops/rules/project/domain/sample/rules.md`
- `agent-ops/rules/project/domain/framework/rules.md`
- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/structured-automation-surface.md`
- `pubspec.yaml`
- `analysis_options.yaml`
- `lib/cli/commands/command_base.dart`
- `lib/cli/commands/command_manager.dart`
- `lib/cli/commands/command_template.dart`
- `lib/cli/cli.dart`
- `bin/main.dart`
- `lib/oto/commands/command.dart`
- `lib/oto/commands/command_registry.dart`
- `test/oto_command_catalog_test.dart`
- `test/oto_application_test.dart`
- `test/oto_cli_runtime_test.dart`
- `test/oto_scheduler_runtime_test.dart`

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

- Existing tests cover command registration/spec/sample consistency, but no CLI command exposes catalog output.
- Existing CLI tests cover printer/runtime and scheduler routing, but not `CommandManager` structured-output log suppression.
- This task must add catalog CLI tests for human table, JSON envelope, filters, and `shouldPrintExecuteLog(['--json']) == false`.
- Bin-level smoke is required because JSON must not be polluted by `Execute command: ...`.

### 심볼 참조

- renamed/removed symbol: none.
- New default method: `CommandBase.shouldPrintExecuteLog(List<String> parameters)`. Existing subclasses need no source changes because the default returns true.
- New top-level command class: `CommandCatalogCli`. Existing call sites 없음.

### 분할 판단

- 분할 정책을 계획 파일 선택 전에 평가했다.
- 이 task는 `02+01_catalog_cli`로, `01_command_catalog_contract`에 의존한다. foundation contract와 terminal CLI workflow를 분리했다.
- 이 task 자체는 단일 계획으로 유지한다. CLI hook, command class, registration, tests, README/roadmap update가 모두 `oto catalog` 노출이라는 한 review unit이며, 나누면 JSON purity 검증이 분산된다.

### 범위 결정 근거

- YAML validation, execution result envelope, step event는 이 작업에서 구현하지 않는다. `structured-automation-surface`의 나머지 필수 기능으로 남긴다.
- `Command.specs` 필드 확장과 param schema 추출은 하지 않는다. `01_command_catalog_contract`의 stable fields만 노출한다.
- scheduler 내부와 pipeline 실행 로직은 수정하지 않는다.
- 현재 워킹 트리의 README/로드맵 변경은 이전 기준선 작업 산출물이므로 되돌리지 않는다. 이 task에서는 catalog CLI 설명과 첫 Milestone 체크만 필요한 만큼 보완한다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`. CLI workflow, stdout JSON purity, bin-level smoke가 성공 조건이라 terminal-agent work에 해당한다.

## 구현 체크리스트

- [ ] [CATALOG_CLI-1] structured output 로그 억제 hook을 추가한다.
- [ ] [CATALOG_CLI-2] `oto catalog` CLI 명령을 추가하고 등록한다.
- [ ] [CATALOG_CLI-3] CLI catalog 테스트와 bin-level smoke 검증을 추가한다.
- [ ] [CATALOG_CLI-4] README와 Milestone 상태를 갱신한다.
- [ ] 중간 검증과 최종 검증을 모두 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [CATALOG_CLI-1] Structured Output 로그 억제 Hook

### 문제

`CommandManager.execute()`는 모든 command 실행 전 `Execute command: ...`를 출력한다. `oto catalog --json`이 이 경로를 그대로 타면 stdout 첫 줄이 JSON이 아니어서 AI/자동화가 바로 파싱할 수 없다.

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

```dart
        if (!printHelp) {
          if (parameters.isEmpty) {
            commandItem.printHelp();
          } else {
            await CLI.println('Execute command: $command$argStr',
                color: Color.green);
            try {
              await commandItem.execute(parameters);
```

### 해결 방법

`CommandBase`에 기본 hook을 추가하고, `CommandManager`가 이 hook을 확인한다. 기존 command는 기본 true라 동작이 유지된다.

Before: [command_base.dart](/config/workspace/oto/lib/cli/commands/command_base.dart:7)

```dart
abstract class CommandBase {
  Map<String, List<String>> get arguments => {};
  String get name;
  String get usage => '';

  Future execute(List<String> parameters);
```

After:

```dart
abstract class CommandBase {
  Map<String, List<String>> get arguments => {};
  String get name;
  String get usage => '';

  bool shouldPrintExecuteLog(List<String> parameters) => true;

  Future execute(List<String> parameters);
```

`CommandManager` after:

```dart
            if (commandItem.shouldPrintExecuteLog(parameters)) {
              await CLI.println('Execute command: $command$argStr',
                  color: Color.green);
            }
            try {
              await commandItem.execute(parameters);
```

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

- [ ] `lib/cli/commands/command_base.dart`에 default hook 추가
- [ ] `lib/cli/commands/command_manager.dart`에서 실행 로그 출력 전 hook 확인
- [ ] 기존 command의 help/execute 동작은 유지

### 테스트 작성

작성한다. `CommandCatalogCli.shouldPrintExecuteLog(['--json'])`는 false, 일반 인자는 true를 테스트한다.

### 중간 검증

```bash
dart test test/oto_catalog_cli_test.dart
```

예상 결과: all tests passed.

## [CATALOG_CLI-2] `oto catalog` CLI 추가

### 문제

현재 top-level CLI 등록 목록에는 template/exe/scheduler만 있다. command catalog를 조회할 안정된 CLI 경로가 없다.

Before: [main.dart](/config/workspace/oto/bin/main.dart:12)

```dart
    CLI.initialize(appName, arguments,
        [CommandTemplate(), CommandExe(), CommandScheduler()]);
```

### 해결 방법

새 파일 `lib/cli/commands/command_catalog.dart`를 추가한다. class 이름은 command 도메인의 `CommandCatalog`와 충돌하지 않게 `CommandCatalogCli`로 둔다.

지원 명령:

```bash
oto catalog
oto catalog --json
oto catalog --category build
oto catalog --command BuildiOS
oto catalog --command BuildiOS --json
```

JSON envelope:

```json
{
  "schemaVersion": 1,
  "type": "commandCatalog",
  "filters": {
    "category": null,
    "command": null
  },
  "commands": []
}
```

CLI 등록 after:

```dart
import 'package:oto/cli/commands/command_catalog.dart';

void main(List<String> arguments) async {
  Application('oto', 'com.toki-labs.oto', () {
    CLI.initialize(appName, arguments, [
      CommandTemplate(),
      CommandExe(),
      CommandScheduler(),
      CommandCatalogCli(),
    ]);
  }, (error, stack) {
```

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

- [ ] `lib/cli/commands/command_catalog.dart` 새 파일 추가
- [ ] `dart:convert`로 JSON 출력 생성
- [ ] `--json`, `--category <value>`, `--category=<value>`, `--command <value>`, `--command=<value>` 파싱
- [ ] unknown option은 `Exception`으로 실패시키고 manager의 기존 error 출력 사용
- [ ] 사람용 기본 출력은 command/category/dataModel/sample/sampleStatus table 제공
- [ ] `bin/main.dart`에 import와 `CommandCatalogCli()` 등록 추가

### 테스트 작성

작성한다. `test/oto_catalog_cli_test.dart`에서 print function을 주입해 사람용/JSON 출력과 필터를 검증한다.

### 중간 검증

```bash
dart test test/oto_catalog_cli_test.dart
```

예상 결과: all tests passed.

## [CATALOG_CLI-3] CLI 테스트와 Smoke 검증

### 문제

직접 class를 호출하는 테스트만으로는 `CommandManager`의 실행 로그가 JSON stdout에 섞이는 회귀를 잡기 어렵다.

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

```dart
            await CLI.println('Execute command: $command$argStr',
                color: Color.green);
```

### 해결 방법

`test/oto_catalog_cli_test.dart`에 아래 테스트를 포함한다.

```dart
test('catalog json output is parseable envelope', () async {
  final lines = <String>[];
  final command = CommandCatalogCli(printString: (value) async {
    lines.add(value);
  });
  await command.execute(['--json', '--command', 'Print']);

  final decoded = jsonDecode(lines.join('\n')) as Map<String, dynamic>;
  expect(decoded['schemaVersion'], 1);
  expect(decoded['type'], 'commandCatalog');
  expect(decoded['commands'], hasLength(1));
});
```

최종 검증에 bin-level smoke를 포함해 stdout 첫 문자가 `{`인지 확인한다.

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

- [ ] `test/oto_catalog_cli_test.dart` 새 파일 추가
- [ ] 사람용 table 출력 테스트
- [ ] JSON envelope parse 테스트
- [ ] category/command filter 테스트
- [ ] `shouldPrintExecuteLog` 테스트
- [ ] 최종 검증에 `dart run bin/main.dart catalog --json` smoke 포함

### 테스트 작성

작성한다. 새 CLI public command이므로 normal + boundary filter + JSON purity 테스트가 필요하다.

### 중간 검증

```bash
dart test test/oto_catalog_cli_test.dart
```

예상 결과: all tests passed.

## [CATALOG_CLI-4] README와 Milestone 갱신

### 문제

README의 `커맨드 카탈로그` 섹션은 내부 진실 소스만 설명한다. 구현 후에는 사용자가 실행할 CLI 조회 경로와 JSON 옵션도 보여야 한다. Milestone 첫 필수 기능도 evidence와 함께 완료 처리해야 한다.

Before: [README.md](/config/workspace/oto/README.md:162)

```markdown
## 커맨드 카탈로그

등록된 커맨드는 `Command.specs`와 `Command.catalogRows`가 단일 진실 소스다.
샘플은 `assets/yaml/sample/**`에서 확인한다.
```

### 해결 방법

README에 `oto catalog` 예시를 추가한다. `agent-ops/roadmap/milestones/structured-automation-surface.md`의 첫 체크박스를 완료로 바꾸고 완료 근거를 짧게 추가한다. 나머지 YAML validation/result/event 체크박스는 그대로 둔다.

After:

```markdown
```bash
oto catalog
oto catalog --json
oto catalog --category build
oto catalog --command BuildiOS --json
```
```

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

- [ ] `README.md` 커맨드 카탈로그 섹션 보완
- [ ] `agent-ops/roadmap/milestones/structured-automation-surface.md` 첫 필수 기능만 완료 처리
- [ ] 나머지 Milestone 필수 기능은 미완료 유지

### 테스트 작성

별도 테스트는 작성하지 않는다. README/로드맵 변경은 smoke와 `dart analyze`로 간접 검증한다.

### 중간 검증

```bash
rg --sort path -n "oto catalog|command catalog를 CLI" README.md agent-ops/roadmap/milestones/structured-automation-surface.md
```

예상 결과: README와 Milestone에서 관련 줄이 검색된다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/cli/commands/command_base.dart` | CATALOG_CLI-1 |
| `lib/cli/commands/command_manager.dart` | CATALOG_CLI-1 |
| `lib/cli/commands/command_catalog.dart` | CATALOG_CLI-2 |
| `bin/main.dart` | CATALOG_CLI-2 |
| `test/oto_catalog_cli_test.dart` | CATALOG_CLI-3 |
| `README.md` | CATALOG_CLI-4 |
| `agent-ops/roadmap/milestones/structured-automation-surface.md` | CATALOG_CLI-4 |

## 최종 검증

```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
output="$(dart run bin/main.dart catalog --json)"; test "$(printf '%s' "$output" | head -c 1)" = "{"; printf '%s\n' "$output" | rg --sort path '"type": "commandCatalog"'
dart run bin/main.dart catalog --category build
rg --sort path -n "oto catalog|command catalog를 CLI" README.md agent-ops/roadmap/milestones/structured-automation-surface.md
```

예상 결과: 모든 명령이 0으로 종료한다. JSON smoke의 첫 글자는 `{`이고 `"type": "commandCatalog"`가 검색된다.

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