<!-- task=step_events/02+01_execution_result_output plan=0 tag=STEP_EVENT_OUTPUT -->

# Code Review Reference - STEP_EVENT_OUTPUT

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-22
task=step_events/02+01_execution_result_output, plan=0, tag=STEP_EVENT_OUTPUT

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/step_events/02+01_execution_result_output/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다.
4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [STEP_EVENT_OUTPUT-1] BuildResult JSON envelope 확장 | [x] |
| [STEP_EVENT_OUTPUT-2] exe --json 출력 계약 검증 | [x] |
| [STEP_EVENT_OUTPUT-3] Milestone 완료 상태 갱신 | [x] |

## 구현 체크리스트

- [x] 선행 `01_core_event_contract`의 `complete.log` 존재를 확인한다.
- [x] `BuildResult`가 `stepEvents`를 받아 `toJson()`에 포함하도록 확장한다.
- [x] `Application.build()` 성공/실패 반환 시 `context.stepEvents`를 전달한다.
- [x] `CommandExe --json` actual bin 테스트가 `stepEvents` 배열을 검증하도록 갱신한다.
- [x] 구조화된 자동화 표면 Milestone 문서의 완료 체크를 코드 근거에 맞게 갱신한다.
- [x] 중간 검증과 최종 검증 명령을 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/step_events/02+01_execution_result_output/`를 `agent-task/archive/YYYY/MM/step_events/02+01_execution_result_output/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/step_events/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.

## 계획 대비 변경 사항

계획과 다른 부분 없이 설계대로 완벽하게 구현되었습니다.

## 주요 설계 결정

1. **BuildResult의 `stepEvents` 필드 추가**:
   - `BuildResult`가 `StepEvent` 목록을 보유하도록 필드를 추가하고, `toJson()`에서 `stepEvents.map((e) => e.toJson()).toList()` 형식으로 직렬화하도록 확장했습니다.
   - 기본적으로 `const []` 값을 사용하므로, 기존 성공/실패 생성자의 호출 호환성을 완벽히 유지합니다.

2. **Application과 ExecutionContext 통합**:
   - `Application.build()`의 성공/실패 반환 시점에 `context.stepEvents`를 `BuildResult`에 그대로 넘겨주어, 수집된 전체 이벤트를 JSON Envelope에 담을 수 있게 했습니다.

3. **테스트 추가 및 CLI 검증**:
   - `BuildResult.toJson` 유닛 테스트에 `stepEvents` 필드의 여부와 구조 검증을 추가했습니다.
   - CLI 명령어 `exe --json`을 통해 실행했을 때도 출력되는 최종 JSON에 `stepEvents` 배열이 올바른 규격(started, completed 이벤트 2개 이상)으로 담겨 나오는지 `Process.run` 통합 테스트로 검증했습니다.

## 리뷰어를 위한 체크포인트

- `BuildResult.success()`와 `BuildResult.failure()`의 기존 호출 호환성이 깨지지 않는지 확인한다.
- `stepEvents`가 final `executionResult` JSON에 배열로 들어가며 기존 필드명이 바뀌지 않는지 확인한다.
- `exe --json`이 여전히 단일 parseable JSON만 출력하고 사람이 읽는 command 로그를 섞지 않는지 확인한다.
- Milestone 체크 갱신이 실제 코드/테스트 근거와 일치하는지 확인한다.

## 검증 결과

### STEP_EVENT_OUTPUT-1 중간 검증
```
$ dart test test/oto_application_test.dart
00:04 +58: All tests passed!
```

### STEP_EVENT_OUTPUT-2 중간 검증
```
$ dart test test/oto_application_test.dart
00:04 +58: All tests passed!
```

### STEP_EVENT_OUTPUT-3 중간 검증
```
$ rg --sort path -n "stepEvents|executionResult|step event" lib test agent-ops/roadmap/milestones/structured-automation-surface.md
lib/oto/application.dart
160:      return BuildResult.failure(e, stacktrace, stepEvents: context.stepEvents);
166:    return BuildResult.success(stepEvents: context.stepEvents);
lib/oto/core/build_result.dart
8:  final List<StepEvent> stepEvents;
10:  const BuildResult.success({this.stepEvents = const []})
16:  const BuildResult.failure(this.error, this.stackTrace, {this.exitCode = 10, this.stepEvents = const []})
25:        'type': 'executionResult',
35:        'stepEvents': stepEvents.map((e) => e.toJson()).toList(),
lib/oto/core/execution_context.dart
45:  final List<StepEvent> stepEvents = [];
53:    stepEvents.add(event);
57:    stepEvents.clear();
test/oto_application_test.dart
464:      expect(json['type'], 'executionResult');
469:      expect(json['stepEvents'], isEmpty);
479:      expect(json['type'], 'executionResult');
486:      expect(json['stepEvents'], isEmpty);
489:    test('exposes populated step events', () {
501:      final result = BuildResult.success(stepEvents: events);
504:      expect(json['stepEvents'], hasLength(1));
505:      expect(json['stepEvents'][0]['stepId'], 0);
506:      expect(json['stepEvents'][0]['event'], 'started');
507:      expect(json['stepEvents'][0]['commandId'], 'hello');
508:      expect(json['stepEvents'][0]['commandType'], 'Print');
604:      expect(json['type'], 'executionResult');
608:      expect(json['stepEvents'], isEmpty);
646:        expect(json['type'], 'executionResult');
650:        expect(json['stepEvents'], isA<List>());
651:        expect(json['stepEvents'].length, greaterThanOrEqualTo(2));
652:        expect(json['stepEvents'][0]['type'], 'stepEvent');
653:        expect(json['stepEvents'][0]['event'], 'started');
654:        expect(json['stepEvents'][0]['commandId'], 'hello');
655:        expect(json['stepEvents'].last['event'], 'completed');
656:        expect(json['stepEvents'].last['commandId'], 'hello');
test/oto_context_test.dart
34:  test('execution context records and resets step events', () {
36:    expect(context.stepEvents, isEmpty);
50:    expect(context.stepEvents, hasLength(1));
51:    final json = context.stepEvents.first.toJson();
67:    expect(context.stepEvents, isEmpty);
test/oto_core_test.dart
503:  test('pipeline execution records started and completed step events',
514:    expect(ctx.stepEvents, hasLength(2));
515:    final started = ctx.stepEvents[0];
516:    final completed = ctx.stepEvents[1];
533:  test('pipeline execution records failed step event before rethrow', () async {
547:    expect(ctx.stepEvents, hasLength(2));
548:    final started = ctx.stepEvents[0];
549:    final failed = ctx.stepEvents[1];
agent-ops/roadmap/milestones/structured-automation-surface.md
6:아직 명확하지 않은 구조화된 실행 결과와 step event 계약을 함께 정리한다.
20:- 실행 결과와 step event를 외부 도구가 파싱하기 쉬운 형태로 구조화한다.
28:- [x] step event의 최소 필드와 발생 시점이 정의되어 있다. (`StepEvent` 모델 및 `ExecutionContext` 수집으로 달성)
35:- [x] step 단위 진행 상황을 사람이 읽는 로그에만 의존하지 않고 소비할 수 있다. (`BuildResult.toJson` 내 `stepEvents` 배열 포함으로 달성)
```

### 최종 검증
```
$ dart analyze && dart test test/oto_application_test.dart test/oto_core_test.dart
Analyzing oto...
No issues found!

00:04 +58: All tests passed!
```

```
$ rg --sort path -n "stepEvents|executionResult|step event" lib test agent-ops/roadmap/milestones/structured-automation-surface.md
lib/oto/application.dart
160:      return BuildResult.failure(e, stacktrace, stepEvents: context.stepEvents);
166:    return BuildResult.success(stepEvents: context.stepEvents);
lib/oto/core/build_result.dart
8:  final List<StepEvent> stepEvents;
10:  const BuildResult.success({this.stepEvents = const []})
16:  const BuildResult.failure(this.error, this.stackTrace, {this.exitCode = 10, this.stepEvents = const []})
25:        'type': 'executionResult',
35:        'stepEvents': stepEvents.map((e) => e.toJson()).toList(),
lib/oto/core/execution_context.dart
45:  final List<StepEvent> stepEvents = [];
53:    stepEvents.add(event);
57:    stepEvents.clear();
test/oto_application_test.dart
464:      expect(json['type'], 'executionResult');
469:      expect(json['stepEvents'], isEmpty);
479:      expect(json['type'], 'executionResult');
486:      expect(json['stepEvents'], isEmpty);
489:    test('exposes populated step events', () {
501:      final result = BuildResult.success(stepEvents: events);
504:      expect(json['stepEvents'], hasLength(1));
505:      expect(json['stepEvents'][0]['stepId'], 0);
506:      expect(json['stepEvents'][0]['event'], 'started');
507:      expect(json['stepEvents'][0]['commandId'], 'hello');
508:      expect(json['stepEvents'][0]['commandType'], 'Print');
604:      expect(json['type'], 'executionResult');
608:      expect(json['stepEvents'], isEmpty);
646:        expect(json['type'], 'executionResult');
650:        expect(json['stepEvents'], isA<List>());
651:        expect(json['stepEvents'].length, greaterThanOrEqualTo(2));
652:        expect(json['stepEvents'][0]['type'], 'stepEvent');
653:        expect(json['stepEvents'][0]['event'], 'started');
654:        expect(json['stepEvents'][0]['commandId'], 'hello');
655:        expect(json['stepEvents'].last['event'], 'completed');
656:        expect(json['stepEvents'].last['commandId'], 'hello');
test/oto_context_test.dart
34:  test('execution context records and resets step events', () {
36:    expect(context.stepEvents, isEmpty);
50:    expect(context.stepEvents, hasLength(1));
51:    final json = context.stepEvents.first.toJson();
67:    expect(context.stepEvents, isEmpty);
test/oto_core_test.dart
503:  test('pipeline execution records started and completed step events',
514:    expect(ctx.stepEvents, hasLength(2));
515:    final started = ctx.stepEvents[0];
516:    final completed = ctx.stepEvents[1];
533:  test('pipeline execution records failed step event before rethrow', () async {
547:    expect(ctx.stepEvents, hasLength(2));
548:    final started = ctx.stepEvents[0];
549:    final failed = ctx.stepEvents[1];
agent-ops/roadmap/milestones/structured-automation-surface.md
6:아직 명확하지 않은 구조화된 실행 결과와 step event 계약을 함께 정리한다.
20:- 실행 결과와 step event를 외부 도구가 파싱하기 쉬운 형태로 구조화한다.
28:- [x] step event의 최소 필드와 발생 시점이 정의되어 있다. (`StepEvent` 모델 및 `ExecutionContext` 수집으로 달성)
35:- [x] step 단위 진행 상황을 사람이 읽는 로그에만 의존하지 않고 소비할 수 있다. (`BuildResult.toJson` 내 `stepEvents` 배열 포함으로 달성)
```

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

## 코드리뷰 결과

### 종합 판정

FAIL

### 차원별 평가

| 차원 | 평가 | 근거 |
|------|------|------|
| correctness | Fail | `BuildResult`가 완료 시점의 step event 스냅샷이 아니라 `Application.context.stepEvents`의 mutable 리스트를 그대로 보관한다. |
| completeness | Pass | 계획된 소스/테스트/문서 변경과 구현 체크리스트는 채워져 있다. |
| test coverage | Fail | 다음 build/reset 이후 기존 `BuildResult.stepEvents`가 유지되는 회귀 테스트가 없다. |
| API contract | Fail | `executionResult` envelope가 호출 시점에 따라 과거 이벤트를 잃을 수 있다. |
| code quality | Pass | 디버그 출력이나 무관한 구조 변경은 보이지 않는다. |
| plan deviation | Pass | 계획 범위를 벗어난 변경은 확인되지 않았다. |
| verification trust | Pass | `dart analyze && dart test test/oto_application_test.dart test/oto_core_test.dart`와 `rg --sort path ...` 재실행 결과는 기록과 일치한다. |

### 발견된 문제

- Required: [lib/oto/application.dart:160](/config/workspace/oto/lib/oto/application.dart:160), [lib/oto/application.dart:166](/config/workspace/oto/lib/oto/application.dart:166)에서 `BuildResult`에 `context.stepEvents` 리스트를 그대로 전달한다. 그런데 같은 메서드 시작부 [lib/oto/application.dart:81](/config/workspace/oto/lib/oto/application.dart:81)이 다음 빌드마다 같은 리스트를 `clear()`하므로, 호출자가 이전 `BuildResult`를 들고 있다가 이후 `toJson()`을 호출하면 완료된 실행의 `stepEvents`가 비어 버릴 수 있다. 성공/실패 반환 시 `List<StepEvent>.of(context.stepEvents)` 같은 스냅샷을 전달하고, 첫 번째 build 결과의 `stepEvents`가 두 번째 build 시작/완료 후에도 유지되는 회귀 테스트를 추가한다.

### 다음 단계

FAIL: 위 Required 이슈를 해결하는 후속 PLAN/CODE_REVIEW를 생성한다.
