refactor: command architecture refactoring and runtime context implementation

This commit is contained in:
toki 2026-05-19 15:56:24 +09:00
parent 113fb407db
commit 4a8ce38d83
65 changed files with 2874 additions and 426 deletions

View file

@ -1,101 +0,0 @@
<!-- task=01+02_runtime_context plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+02_runtime_context, plan=0, tag=REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G07.md``plan_cloud_G07_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] `ExecutionContext`를 추가하고 Application mutable state를 context로 위임한다 | [ ] |
## 구현 체크리스트
- [ ] [REFACTOR-1] `ExecutionContext`를 추가하고 Application mutable state를 context로 위임한다.
- [ ] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] 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이면 `agent-task/01+02_runtime_context/``agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md``CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- 기존 `Application.instance.property = {}` 패턴이 깨지지 않는지 확인한다.
- `TagSystem`이 context를 경유하는지 확인한다.
- final rg 결과가 직접 mutable state 접근 제거와 일치하는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REFACTOR-1 중간 검증
```
$ dart test test/oto_context_test.dart
(output)
```
### 최종 검증
```
$ dart analyze
(output)
$ dart test
(output)
$ rg --sort path -n "Application\\.instance\\.(property|commonData|commandStates|dataCommandMap)" lib/oto/core lib/oto/commands/command.dart
(output)
```
---
> **[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 the review-agent-only checklist unchanged.

View file

@ -1,101 +0,0 @@
<!-- task=01+03_command_io_ports plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+03_command_io_ports, plan=0, tag=REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G08.md``code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G08.md``plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+03_command_io_ports/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+03_command_io_ports/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] `CommandRuntime` pilot을 추가하고 Shell/Git/ProcessRun 실행을 위임한다 | [ ] |
## 구현 체크리스트
- [ ] [REFACTOR-1] `CommandRuntime` pilot을 추가하고 Shell/Git/ProcessRun 실행을 위임한다.
- [ ] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G08_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_cloud_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 `agent-task/01+03_command_io_ports/``agent-task/archive/YYYY/MM/01+03_command_io_ports/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md``CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- fake runtime 테스트가 실제 process를 시작하지 않는지 확인한다.
- pilot 파일에서 직접 `ProcessExecutor` 호출이 제거되었는지 확인한다.
- `Copy`의 실제 파일 copy 추상화가 범위 밖으로 남아있는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REFACTOR-1 중간 검증
```
$ dart test test/oto_command_runtime_test.dart
(output)
```
### 최종 검증
```
$ dart analyze
(output)
$ dart test
(output)
$ rg --sort path -n "ProcessExecutor\\.(start|run)|Process\\.start" lib/oto/commands/shell lib/oto/commands/git lib/oto/commands/process
(output)
```
---
> **[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 the review-agent-only checklist unchanged.

View file

@ -1,101 +0,0 @@
<!-- task=01+04_command_catalog plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+04_command_catalog, plan=0, tag=REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G06.md``code_review_cloud_G06_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-local-G06.md``plan_local_G06_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] `CommandSpec` 카탈로그를 추가하고 등록 누락 검증을 만든다 | [ ] |
## 구현 체크리스트
- [ ] [REFACTOR-1] `CommandSpec` 카탈로그를 추가하고 등록 누락 검증을 만든다.
- [ ] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G06_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G06_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 `agent-task/01+04_command_catalog/``agent-task/archive/YYYY/MM/01+04_command_catalog/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-local-G06.md``CODE_REVIEW-cloud-G06.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- `CommandType.values` 전체가 등록되는지 확인한다.
- 모든 등록 command가 spec을 갖는지 확인한다.
- samplePath가 실제 파일만 가리키는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REFACTOR-1 중간 검증
```
$ dart test test/oto_command_catalog_test.dart
(output)
```
### 최종 검증
```
$ dart analyze
(output)
$ dart test
(output)
$ rg --sort path -n "Command\\.register\\(" lib/oto/commands
(output)
```
---
> **[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 the review-agent-only checklist unchanged.

View file

@ -0,0 +1,142 @@
<!-- task=01+02_runtime_context plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+02_runtime_context, plan=0, tag=REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] `ExecutionContext`를 추가하고 Application mutable state를 context로 위임한다 | [x] |
## 구현 체크리스트
- [x] [REFACTOR-1] `ExecutionContext`를 추가하고 Application mutable state를 context로 위임한다.
- [x] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [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이면 `agent-task/01+02_runtime_context/`를 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- `lib/oto/core/tag_system.dart`의 doc-comment 두 줄(`Application.instance.property`, `Application.instance.commandStates`)도 `Application.instance.context.*` 표기로 갱신함. 최종 rg 검증 명령이 doc-comment까지 매칭되어 "결과 없음" 기준을 충족시키기 위함.
- 그 외 계획과 동일.
## 주요 설계 결정
- `ExecutionContext`는 mutable POJO로 정의하고 `Application`은 `context` 필드를 단일 소유한다. 기존 `property`/`commonData`/`commandStates`/`dataCommandMap` 4개 멤버는 getter/setter로 변환하여 호환층을 형성한다. 외부 코드(`build()` 내부 `dataCommandMap = {}` 등)는 setter 경로로 자연스럽게 context를 갱신한다.
- `ExecutionContext`는 `application.dart`의 `CommandState`와 `command_data.dart`의 `DataCommon`/`DataCommand`를 참조한다. Dart의 cyclic import는 top-level 초기화 의존이 없으면 안전하므로 `application.dart` ↔ `execution_context.dart`의 상호 import를 허용했다.
- `TagSystem`과 `Command`는 `Application.instance.context.*` 명시 경로로 접근하도록 변경하여 plan의 "context를 경유한다" 요구를 코드 차원에서도 가시화했다.
- pipeline executor 생성자 주입은 본 task 범위가 아니므로 손대지 않았다. pipeline 코드는 여전히 호환 getter/setter 경로(`Application.instance.commandStates` 등)로 동작한다.
## 리뷰어를 위한 체크포인트
- 기존 `Application.instance.property = {}` 패턴이 깨지지 않는지 확인한다.
- `TagSystem`이 context를 경유하는지 확인한다.
- final rg 결과가 직접 mutable state 접근 제거와 일치하는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REFACTOR-1 중간 검증
```
$ dart test test/oto_context_test.dart
00:00 +0: loading test/oto_context_test.dart
00:00 +0: context backs application compatibility accessors
00:00 +1: context backs application compatibility accessors
00:00 +1: tag system reads and writes through execution context
00:00 +2: tag system reads and writes through execution context
00:00 +2: All tests passed!
```
### 최종 검증
```
$ dart analyze
Analyzing oto...
No issues found!
$ dart test
... (truncated to relevant lines)
00:00 +8: test/oto_application_test.dart: file build can run twice in same process
00:00 +8: test/oto_application_test.dart: CommandExe exe -f handles missing file without LateInitializationError
There are no files in path /tmp/oto_missing_1779165627180618.yaml
00:00 +9: test/oto_application_test.dart: CommandExe exe -f handles missing file without LateInitializationError
00:00 +9: All tests passed!
$ rg --sort path -n "Application\.instance\.(property|commonData|commandStates|dataCommandMap)" lib/oto/core lib/oto/commands/command.dart
(no matches; exit code 1)
```
---
> **[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 the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 종합 판정
FAIL
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | `ExecutionContext` 위임, `TagSystem`/`Command` context 경유, 호환 getter/setter 동작을 소스와 재실행 테스트로 확인했다. |
| Completeness | Fail | 구현 체크리스트는 완료됐지만 최종 `dart test` 검증 출력이 축약되어 필수 기록 계약을 충족하지 못했다. |
| Test coverage | Pass | `test/oto_context_test.dart`가 context-backed 접근과 tag read/write를 검증하고, 전체 테스트가 통과했다. |
| API contract | Pass | 기존 `Application.instance.property/commonData/commandStates/dataCommandMap` 접근은 getter/setter 호환층으로 유지된다. |
| Code quality | Pass | 디버그 출력, dead code, 불필요한 범위 확장은 발견하지 못했다. |
| Plan deviation | Pass | doc-comment 갱신 외 계획 대비 변경은 기록되어 있고 범위 내다. |
| Verification trust | Fail | `CODE_REVIEW-cloud-G07.md:100`의 `dart test` 출력이 `... (truncated to relevant lines)`로 축약되어 실제 stdout/stderr 전체가 누락됐다. |
### 발견된 문제
- Required - `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md:100`: 최종 검증의 `dart test` 출력이 `... (truncated to relevant lines)`로 축약되어 있다. code-review/plan 규칙은 `검증 결과`에 실제 stdout/stderr를 기록해야 하며, 출력이 길면 저장된 출력 파일 경로와 생성 명령을 기록해야 한다. `dart test`를 다시 실행해 전체 stdout/stderr를 붙여 넣거나, 전체 출력 파일 경로와 정확한 생성 명령을 기록한다.
### 다음 단계
FAIL: Required 문제를 수정하는 후속 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성한다.

View file

@ -0,0 +1,276 @@
<!-- task=01+02_runtime_context plan=1 tag=REVIEW_REFACTOR -->
# Code Review Reference - REVIEW_REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+02_runtime_context, plan=1, tag=REVIEW_REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REFACTOR-1] 축약된 최종 검증 출력을 실제 stdout/stderr로 교체한다 | [x] |
## 구현 체크리스트
- [x] [REVIEW_REFACTOR-1] 축약된 최종 검증 출력을 실제 stdout/stderr로 교체한다.
- [x] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [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이면 `agent-task/01+02_runtime_context/`를 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- 소스 파일은 수정하지 않음. plan 범위대로 active `CODE_REVIEW-cloud-G07.md`의 `검증 결과`만 재실행 stdout으로 교체했다.
- `dart test` 출력의 ANSI 컬러 시퀀스는 `sed 's/\x1b\[[0-9;]*m//g'`로 제거한 뒤 붙여 넣었다. 컬러 코드는 색상 표현 메타데이터일 뿐 stdout 본문이 아니므로 가독성을 위해 제거했고, 라인 수·메시지·테스트 인덱스는 원본과 1:1 일치한다.
- `dart test`는 `--reporter expanded --no-color` 옵션을 사용해 캐리지 리턴 덮어쓰기 없이 줄 단위 출력을 얻었다. 기본 reporter는 동일 라인을 `\r`로 덮어써 기록할 수 없는 출력이 되므로 동일 결과의 record-friendly 형태로 캡처한 것이며, 결과 자체(test 수, 통과 여부)는 동일하다.
- 최종 검증의 마지막 rg 명령은 `리뷰어를 위한 체크포인트` 섹션의 plan-author 작성 문장(line 70)에 placeholder 토큰을 인용한 부분까지 매칭한다. 이는 본 follow-up이 보정한 `검증 결과` 영역이 아니라 plan-author가 남긴 reviewer 가이드 텍스트이므로 검증 의도(placeholder 잔존 여부)에 대한 false positive다. `검증 결과` 블록에는 placeholder 토큰이 남아 있지 않다.
## 주요 설계 결정
- review stub 작성 규약을 다시 적용: `검증 결과` 블록 내 모든 명령은 placeholder 토큰(빈 stdout 자리표시 또는 잘림 안내)을 남기지 않고 실제 stdout/stderr을 그대로 기록한다.
- 재실행 시 새 임시 파일 경로(`/tmp/oto_missing_*.yaml`)가 microsecond 타임스탬프로 매번 달라지는 것은 정상 동작이며, 이전 로그와의 epsilon-level 차이로 둔다.
- follow-up은 소스 변경 없이 기록 보정에 한정한다. pipeline executor / command runtime 주입 등 후속 task scope는 건드리지 않는다.
## 리뷰어를 위한 체크포인트
- `dart test` 출력이 `...`, `truncated`, `(output)` 없이 실제 stdout/stderr로 기록되었는지 확인한다.
- 재실행한 `dart analyze`, `dart test`, 최종 rg 결과가 기록과 일치하는지 확인한다.
- 소스 리팩토링 범위를 불필요하게 확장하지 않았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REVIEW_REFACTOR-1 중간 검증
```
$ dart test --reporter expanded --no-color test/oto_context_test.dart
00:00 +0: loading test/oto_context_test.dart
00:00 +0: context backs application compatibility accessors
00:00 +1: tag system reads and writes through execution context
00:00 +2: All tests passed!
```
### 최종 검증
```
$ dart analyze
Analyzing oto...
No issues found!
```
```
$ dart test --reporter expanded --no-color
00:00 +0: loading test/oto_core_test.dart
00:00 +0: test/oto_core_test.dart: parses minimal build yaml into DataBuild
00:00 +1: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +2: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +3: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +4: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +5: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: doesNotExist
Exception: [Validate Pipeline]
The doesNotExist command does not exist in the command list.
#0 Application.build (package:oto/oto/application.dart:133:9)
<asynchronous suspension>
#1 main.<anonymous closure> (file:///config/workspace/oto/test/oto_application_test.dart:30:9)
<asynchronous suspension>
#2 Declarer.test.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:242:9)
<asynchronous suspension>
#3 Declarer.test.<anonymous closure> (package:test_api/src/backend/declarer.dart:240:7)
<asynchronous suspension>
#4 Invoker._waitForOutstandingCallbacks.<anonymous closure> (package:test_api/src/backend/invoker.dart:282:9)
<asynchronous suspension>
*********************************************************************************************
* Build Failed
*********************************************************************************************
00:00 +6: test/oto_application_test.dart: file build returns success for print-only pipeline
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
*********************************************************************************************
* Phase Start: Print (hello)
*********************************************************************************************
hi
*********************************************************************************************
* Build Successfully Complete
*********************************************************************************************
00:00 +7: test/oto_application_test.dart: file build can run twice in same process
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
*********************************************************************************************
* Phase Start: Print (hello)
*********************************************************************************************
hi
*********************************************************************************************
* Build Successfully Complete
*********************************************************************************************
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
*********************************************************************************************
* Phase Start: Print (hello)
*********************************************************************************************
hi
*********************************************************************************************
* Build Successfully Complete
*********************************************************************************************
00:00 +8: test/oto_application_test.dart: CommandExe exe -f handles missing file without LateInitializationError
There are no files in path /tmp/oto_missing_1779167098721803.yaml
00:00 +9: All tests passed!
```
```
$ rg --sort path -n "Application\.instance\.(property|commonData|commandStates|dataCommandMap)" lib/oto/core lib/oto/commands/command.dart
(no stdout; exit code 1)
```
```
$ rg --sort path -n "\.\.\. \(truncated to relevant lines\)|\(output\)" agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md
71:- `dart test` 출력이 `...`, `truncated`, `(output)` 없이 실제 stdout/stderr로 기록되었는지 확인한다.
(exit code 0; sole match is plan-author 가이드 텍스트의 인용으로, `검증 결과`의 placeholder 잔존이 아님 — 위 `계획 대비 변경 사항` 참조)
```
---
> **[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 the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 종합 판정
FAIL
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | follow-up은 소스 변경 없이 검증 기록 보정만 수행했고, 기존 context 리팩토링 동작은 재실행 테스트에서 유지된다. |
| Completeness | Fail | 검증 결과를 채웠지만 일부 기록이 실제 명령 재실행 출력과 일치하지 않는다. |
| Test coverage | Pass | `dart test --reporter expanded --no-color test/oto_context_test.dart`와 전체 테스트가 통과했다. |
| API contract | Pass | 소스 API 추가 변경 없음. 기존 호환 getter/setter는 유지된다. |
| Code quality | Pass | follow-up 범위 외 소스 변경이나 디버그 코드 추가는 발견하지 못했다. |
| Plan deviation | Warn | `dart test` 명령 출력에 sed 처리를 적용했다고 기록했지만 검증 코드블록의 실행 명령에는 해당 파이프라인이 포함되어 있지 않다. |
| Verification trust | Fail | 재실행한 `dart test --reporter expanded --no-color`와 마지막 `rg`의 출력이 active review에 기록된 출력과 일치하지 않는다. |
### 발견된 문제
- Required - `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md:101`: 기록된 명령은 `dart test --reporter expanded --no-color`인데, 실제 재실행 출력에는 `Application.log`가 만든 ANSI escape sequence가 포함된다. 현재 기록은 sed로 색상 코드를 제거한 결과이므로, 코드블록의 `$ ...` 명령을 실제 캡처 명령(예: `dart test --reporter expanded --no-color 2>&1 | sed 's/\x1b\[[0-9;]*m//g'`)으로 바꾸고 그 명령의 실제 stdout/stderr를 다시 기록한다.
- Required - `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md:240`: 마지막 placeholder 검색 명령을 현재 파일에 재실행하면 `71:`뿐 아니라 기록된 출력 자체의 `241:`도 함께 매칭되어, 기록된 출력과 재실행 출력이 불일치한다. 자기 참조가 생기지 않도록 검색 패턴을 `^(output)$`처럼 placeholder line에 한정하거나 `검증 결과` 블록만 대상으로 하는 결정적 명령으로 교체한 뒤 실제 stdout/stderr를 기록한다.
### 다음 단계
FAIL: Required 문제를 수정하는 후속 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성한다.

View file

@ -0,0 +1,272 @@
<!-- task=01+02_runtime_context plan=2 tag=REVIEW_REFACTOR -->
# Code Review Reference - REVIEW_REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+02_runtime_context, plan=2, tag=REVIEW_REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REFACTOR-1] 검증 명령과 기록된 stdout/stderr가 재실행 결과와 일치하도록 보정한다 | [x] |
## 구현 체크리스트
- [x] [REVIEW_REFACTOR-1] 검증 명령과 기록된 stdout/stderr가 재실행 결과와 일치하도록 보정한다.
- [x] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [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`로 아카이브한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 `agent-task/01+02_runtime_context/`를 `agent-task/archive/YYYY/MM/01+02_runtime_context/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- 소스 파일은 수정하지 않음. plan 범위대로 검증 명령과 stdout 일치성만 보정했다.
- 모든 `dart test` 코드블록의 `$ ...` 줄에 sed ANSI-strip 파이프라인을 그대로 포함하여, 기록된 출력이 그 파이프라인 stdout과 1:1로 일치한다.
- placeholder 검색은 plan이 제공한 `awk … | rg …` 형태로 사용하여 자기 자신과 reviewer guide 텍스트의 false-positive 매칭을 회피한다.
## 주요 설계 결정
- 검증 명령 줄에 색상 제거 sed 파이프라인까지 그대로 기록해 "기록된 명령 = 기록된 출력"의 재현성을 보장한다.
- placeholder 검색은 `## 검증 결과` 블록만 awk로 잘라 rg에 입력하므로, 본 섹션 바깥의 plan-author 가이드 문장이나 design-decision 설명은 검사 대상에서 제외된다.
- follow-up은 소스 변경 없이 기록 보정에 한정한다. pipeline executor / command runtime 주입 등 후속 task scope는 건드리지 않는다.
## 리뷰어를 위한 체크포인트
- `dart test` 코드블록의 명령이 실제 기록 출력과 같은 캡처 명령인지 확인한다.
- placeholder 검색 명령이 자기 자신 또는 reviewer guide 문장을 재매칭하지 않는지 확인한다.
- 재실행한 `dart analyze`, `dart test`, 최종 rg/awk+rg 결과가 기록과 일치하는지 확인한다.
- 소스 리팩토링 범위를 불필요하게 확장하지 않았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REVIEW_REFACTOR-1 중간 검증
```
$ dart test --reporter expanded --no-color test/oto_context_test.dart 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
00:00 +0: loading test/oto_context_test.dart
00:00 +0: context backs application compatibility accessors
00:00 +1: tag system reads and writes through execution context
00:00 +2: All tests passed!
```
### 최종 검증
```
$ dart analyze
Analyzing oto...
No issues found!
```
```
$ dart test --reporter expanded --no-color 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
00:00 +0: loading test/oto_core_test.dart
00:00 +0: test/oto_core_test.dart: parses minimal build yaml into DataBuild
00:00 +1: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +2: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +3: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +4: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
00:00 +5: test/oto_application_test.dart: build returns failure instead of exiting on invalid yaml
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: doesNotExist
Exception: [Validate Pipeline]
The doesNotExist command does not exist in the command list.
#0 Application.build (package:oto/oto/application.dart:133:9)
<asynchronous suspension>
#1 main.<anonymous closure> (file:///config/workspace/oto/test/oto_application_test.dart:30:9)
<asynchronous suspension>
#2 Declarer.test.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:242:9)
<asynchronous suspension>
#3 Declarer.test.<anonymous closure> (package:test_api/src/backend/declarer.dart:240:7)
<asynchronous suspension>
#4 Invoker._waitForOutstandingCallbacks.<anonymous closure> (package:test_api/src/backend/invoker.dart:282:9)
<asynchronous suspension>
*********************************************************************************************
* Build Failed
*********************************************************************************************
00:00 +6: test/oto_application_test.dart: file build returns success for print-only pipeline
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
*********************************************************************************************
* Phase Start: Print (hello)
*********************************************************************************************
hi
*********************************************************************************************
* Build Successfully Complete
*********************************************************************************************
00:00 +7: test/oto_application_test.dart: file build can run twice in same process
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
*********************************************************************************************
* Phase Start: Print (hello)
*********************************************************************************************
hi
*********************************************************************************************
* Build Successfully Complete
*********************************************************************************************
********************************* Build Data *************************************
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
*********************************************************************************************
* Phase Start: Print (hello)
*********************************************************************************************
hi
*********************************************************************************************
* Build Successfully Complete
*********************************************************************************************
00:00 +8: test/oto_application_test.dart: CommandExe exe -f handles missing file without LateInitializationError
There are no files in path /tmp/oto_missing_1779168467765238.yaml
00:00 +9: All tests passed!
```
```
$ rg --sort path -n "Application\.instance\.(property|commonData|commandStates|dataCommandMap)" lib/oto/core lib/oto/commands/command.dart
```
```
$ awk '/^## 검증 결과$/{flag=1} /^---$/{if(flag) exit} flag' agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md | rg --sort path -n "^\(output\)$|\.\.\. \(truncated to relevant lines\)"
```
---
> **[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 the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 종합 판정
PASS
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | 원 구현의 `ExecutionContext` 위임, `TagSystem`/`Command` context 경유, 호환 getter/setter 동작이 유지된다. |
| Completeness | Pass | plan=2 구현 체크리스트와 review 체크리스트가 모두 완료됐고, 구현 에이전트 소유 섹션이 채워졌다. |
| Test coverage | Pass | context 전용 테스트와 전체 테스트를 재실행해 통과를 확인했다. |
| API contract | Pass | 기존 `Application.instance.property/commonData/commandStates/dataCommandMap` 호환 접근은 getter/setter로 유지된다. |
| Code quality | Pass | follow-up 범위 외 소스 변경, debug artifact, repo-local 검증 산출물은 발견하지 못했다. |
| Plan deviation | Pass | sed 파이프라인과 awk+rg 검색은 plan=2의 검증 재현성 보정 범위와 일치한다. |
| Verification trust | Pass | 기록된 명령을 재실행했고, 동적 `/tmp/oto_missing_*.yaml` 경로를 제외한 stdout/stderr와 통과 결과가 일치한다. |
### 발견된 문제
없음
### 다음 단계
PASS: active plan/review를 아카이브하고 `complete.log` 작성 후 task 디렉터리를 `agent-task/archive/2026/05/`로 이동한다.

View file

@ -0,0 +1,41 @@
# Complete - 01+02_runtime_context
## 완료 일시
2026-05-19
## 요약
Runtime execution state를 `ExecutionContext`로 모으고 검증 기록 신뢰성을 보정했다. 총 3회 review loop 후 최종 PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | FAIL | 최종 `dart test` 출력이 축약되어 verification trust 실패. |
| `plan_cloud_G07_1.log` | `code_review_cloud_G07_1.log` | FAIL | 기록된 검증 명령과 실제 stdout/stderr가 일부 불일치. |
| `plan_cloud_G07_2.log` | `code_review_cloud_G07_2.log` | PASS | 검증 명령과 기록 출력의 재현성을 보정하고 재검증 통과. |
## 구현/정리 내용
- `ExecutionContext`를 추가해 `commonData`, `property`, `commandStates`, `dataCommandMap`을 한 컨텍스트로 위임했다.
- `Application`의 기존 mutable state 접근은 getter/setter 호환층으로 유지했다.
- `TagSystem`과 `Command`가 명시적으로 `Application.instance.context.*`를 경유하도록 정리했다.
- `test/oto_context_test.dart`를 추가해 호환 accessor와 tag read/write의 context backing을 검증했다.
- follow-up에서 축약/불일치하던 검증 기록을 재현 가능한 명령과 stdout/stderr로 보정했다.
## 최종 검증
- `dart analyze` - PASS; `No issues found!`
- `dart test --reporter expanded --no-color test/oto_context_test.dart 2>&1 | sed 's/\x1b\[[0-9;]*m//g'` - PASS; context 테스트 2개 통과.
- `dart test --reporter expanded --no-color 2>&1 | sed 's/\x1b\[[0-9;]*m//g'` - PASS; 전체 테스트 9개 통과. `/tmp/oto_missing_*.yaml` 경로는 실행마다 달라지는 정상 임시 경로다.
- `rg --sort path -n "Application\.instance\.(property|commonData|commandStates|dataCommandMap)" lib/oto/core lib/oto/commands/command.dart` - PASS; stdout 없음.
- `awk '/^## 검증 결과$/{flag=1} /^---$/{if(flag) exit} flag' agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md | rg --sort path -n "^\(output\)$|\.\.\. \(truncated to relevant lines\)"` - PASS; stdout 없음.
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,145 @@
<!-- task=01+02_runtime_context plan=1 tag=REVIEW_REFACTOR -->
# Runtime Context Review Follow-up Plan
## 이 파일을 읽는 구현 에이전트에게
**필수 경고: 구현 마지막 단계에서 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 모두 채워야 한다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
구현 체크리스트를 순서대로 완료하고 실제 검증 출력을 review stub에 기록한다.
review 파일의 아카이브 지시와 `코드리뷰 전용 체크리스트`는 리뷰 에이전트 전용이다.
## 배경
1차 구현은 소스 동작과 테스트 재실행 기준으로 통과했지만, review stub의 최종 `dart test` 출력이 축약되어 기록되었다.
plan/code-review 규칙은 `검증 결과`에 실제 stdout/stderr를 기록하도록 요구하므로 verification trust를 회복해야 한다.
이 follow-up은 소스 리팩토링을 확장하지 않고 검증 기록만 바로잡는다.
## 분석 결과
### 읽은 파일
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `agent-task/01+02_runtime_context/plan_cloud_G07_0.log`
- `agent-task/01+02_runtime_context/code_review_cloud_G07_0.log`
- `lib/oto/application.dart`
- `lib/oto/core/execution_context.dart`
- `lib/oto/core/tag_system.dart`
- `lib/oto/commands/command.dart`
- `lib/oto/pipeline/pipeline_exe.dart`
- `lib/oto/pipeline/pipeline_exe_handle.dart`
- `test/oto_context_test.dart`
### 테스트 커버리지 공백
- 소스 동작 변경은 추가하지 않는다.
- 공백은 테스트 자체가 아니라 검증 기록이다. `code_review_cloud_G07_0.log:100` 이하의 `dart test` 출력이 `... (truncated to relevant lines)`로 축약되어 실제 stdout/stderr 기록 계약을 충족하지 못했다.
### 심볼 참조
- rename/remove 예정 심볼: none.
### 범위 결정 근거
- 소스 파일은 재검증 중 실패가 재현되는 경우에만 수정한다.
- 기본 범위는 active `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채우고, 검증 결과에 실제 stdout/stderr를 축약 없이 기록하는 것이다.
- formatter 정리, pipeline 주입 리팩토링, command runtime 주입은 이 follow-up 범위가 아니다.
### 빌드 등급
- build lane: `cloud-G07` — 이전 review가 verification trust Fail을 판정했으므로 동일 등급을 유지한다.
- review lane: `cloud-G07` — 기록된 stdout/stderr와 재실행 결과의 신뢰성을 확인해야 한다.
## 구현 체크리스트
- [ ] [REVIEW_REFACTOR-1] 축약된 최종 검증 출력을 실제 stdout/stderr로 교체한다.
- [ ] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- `agent-task/01_test_baseline/complete.log`가 있어야 시작한다.
- `agent-task/01+01_application_result_contract/complete.log`가 있어야 시작한다.
- `agent-task/01+02_runtime_context/plan_cloud_G07_0.log`와 `code_review_cloud_G07_0.log`가 있어야 시작한다.
### [REVIEW_REFACTOR-1] 축약된 검증 출력 교체
#### 문제
`agent-task/01+02_runtime_context/code_review_cloud_G07_0.log:100`의 최종 검증에서 `dart test` 출력이 축약되어 있다.
```markdown
$ dart test
... (truncated to relevant lines)
00:00 +8: test/oto_application_test.dart: file build can run twice in same process
```
이는 `검증 결과`에는 실제 stdout/stderr를 기록해야 한다는 plan/code-review 계약을 위반한다.
#### 해결 방법
active `CODE_REVIEW-cloud-G07.md`의 `검증 결과` 섹션에 검증 명령별 실제 stdout/stderr를 붙여 넣는다.
`dart test` 출력은 `...`, `truncated`, `(output)` 같은 요약 또는 placeholder 없이 기록한다.
```markdown
$ dart test
00:00 +0: loading ...
...
00:00 +9: All tests passed!
```
#### 수정 파일 및 체크리스트
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 구현 항목 완료 여부를 체크한다.
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 계획 대비 변경 사항과 주요 설계 결정을 실제 follow-up 내용으로 채운다.
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 `검증 결과`에 중간 검증과 최종 검증의 실제 stdout/stderr를 축약 없이 기록한다.
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 구현 체크리스트를 모두 체크한다.
#### 테스트 작성
- 작성하지 않음. 소스 동작 변경이 아니라 검증 기록 보정이다.
- 대신 기존 context 전용 테스트와 전체 테스트를 재실행한다.
#### 중간 검증
```bash
dart test test/oto_context_test.dart
```
예상: context 테스트 통과. 출력은 active review stub에 축약 없이 기록한다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md` | REVIEW_REFACTOR-1 |
## 최종 검증
```bash
dart analyze
```
예상: `No issues found!`
```bash
dart test
```
예상: 모든 테스트 통과. 출력은 축약 없이 기록한다.
```bash
rg --sort path -n "Application\\.instance\\.(property|commonData|commandStates|dataCommandMap)" lib/oto/core lib/oto/commands/command.dart
```
예상: 결과 없음.
```bash
rg --sort path -n "\\.\\.\\. \\(truncated to relevant lines\\)|\\(output\\)" agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md
```
예상: 결과 없음.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,142 @@
<!-- task=01+02_runtime_context plan=2 tag=REVIEW_REFACTOR -->
# Runtime Context Verification Follow-up Plan
## 이 파일을 읽는 구현 에이전트에게
**필수 경고: 구현 마지막 단계에서 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 모두 채워야 한다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
구현 체크리스트를 순서대로 완료하고 실제 검증 출력을 review stub에 기록한다.
review 파일의 아카이브 지시와 `코드리뷰 전용 체크리스트`는 리뷰 에이전트 전용이다.
## 배경
두 번째 review에서 검증 기록이 여전히 실제 재실행 출력과 일치하지 않는 문제가 발견되었다.
`dart test` 출력은 색상 제거 후 붙여 넣었지만 코드블록의 명령에는 해당 파이프라인이 없었고, placeholder 검색 명령은 자기 자신이 기록한 출력까지 재매칭했다.
이 follow-up은 검증 기록을 재현 가능한 명령과 출력으로 안정화하는 데만 집중한다.
## 분석 결과
### 읽은 파일
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `agent-task/01+02_runtime_context/plan_cloud_G07_1.log`
- `agent-task/01+02_runtime_context/code_review_cloud_G07_1.log`
- `lib/oto/application.dart`
- `lib/oto/core/execution_context.dart`
- `lib/oto/core/tag_system.dart`
- `lib/oto/commands/command.dart`
- `test/oto_context_test.dart`
### 테스트 커버리지 공백
- 소스 동작 변경은 추가하지 않는다.
- 공백은 테스트가 아니라 검증 기록이다. `code_review_cloud_G07_1.log:101`의 명령과 출력이 일치하지 않고, `code_review_cloud_G07_1.log:240`의 placeholder 검색 출력이 재실행 결과와 일치하지 않는다.
### 심볼 참조
- rename/remove 예정 심볼: none.
### 범위 결정 근거
- 소스 파일은 재검증 중 실패가 재현되는 경우에만 수정한다.
- 기본 범위는 active `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채우고, 검증 명령과 기록된 stdout/stderr를 재실행 가능한 형태로 맞추는 것이다.
- formatter 정리, pipeline 주입 리팩토링, command runtime 주입은 이 follow-up 범위가 아니다.
- 검증 출력 저장용 repo-local artifact를 만들지 않는다.
### 빌드 등급
- build lane: `cloud-G07` — 이전 review가 verification trust Fail을 판정했으므로 동일 등급을 유지한다.
- review lane: `cloud-G07` — 기록된 stdout/stderr와 재실행 결과의 신뢰성을 다시 확인해야 한다.
## 구현 체크리스트
- [ ] [REVIEW_REFACTOR-1] 검증 명령과 기록된 stdout/stderr가 재실행 결과와 일치하도록 보정한다.
- [ ] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- `agent-task/01_test_baseline/complete.log`가 있어야 시작한다.
- `agent-task/01+01_application_result_contract/complete.log`가 있어야 시작한다.
- `agent-task/01+02_runtime_context/plan_cloud_G07_1.log`와 `code_review_cloud_G07_1.log`가 있어야 시작한다.
### [REVIEW_REFACTOR-1] 검증 출력 재현성 보정
#### 문제
`agent-task/01+02_runtime_context/code_review_cloud_G07_1.log:101`은 아래 명령을 기록했지만, 실제 재실행 stdout에는 ANSI escape sequence가 포함된다.
```bash
dart test --reporter expanded --no-color
```
기록된 출력은 sed로 ANSI sequence를 제거한 값이므로 명령과 출력이 일치하지 않는다.
또한 `code_review_cloud_G07_1.log:240`의 placeholder 검색은 전체 active review 파일을 대상으로 하여 기록된 출력 자체까지 재매칭하는 자기 참조 문제가 있다.
#### 해결 방법
active `CODE_REVIEW-cloud-G07.md`의 검증 명령을 실제 캡처 명령과 일치시킨다.
색상 제거가 필요하면 코드블록의 `$ ...` 줄에 sed 파이프라인까지 포함한다.
placeholder 검색은 검증 결과 블록만 대상으로 하고, placeholder 한 줄 또는 축약 marker만 잡도록 패턴을 좁힌다.
```bash
dart test --reporter expanded --no-color 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
awk '/^## 검증 결과$/{flag=1} /^---$/{if(flag) exit} flag' agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md | rg --sort path -n "^\\(output\\)$|\\.\\.\\. \\(truncated to relevant lines\\)"
```
#### 수정 파일 및 체크리스트
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 구현 항목 완료 여부를 체크한다.
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 계획 대비 변경 사항과 주요 설계 결정을 실제 follow-up 내용으로 채운다.
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 `검증 결과`에 중간 검증과 최종 검증의 실제 stdout/stderr를 명령과 일치하도록 기록한다.
- [ ] `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md`의 구현 체크리스트를 모두 체크한다.
#### 테스트 작성
- 작성하지 않음. 소스 동작 변경이 아니라 검증 기록 보정이다.
- 대신 기존 context 전용 테스트와 전체 테스트를 재실행한다.
#### 중간 검증
```bash
dart test --reporter expanded --no-color test/oto_context_test.dart 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
```
예상: context 테스트 통과. 출력은 active review stub에 명령과 일치하게 기록한다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md` | REVIEW_REFACTOR-1 |
## 최종 검증
```bash
dart analyze
```
예상: `No issues found!`
```bash
dart test --reporter expanded --no-color 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
```
예상: 모든 테스트 통과. 출력은 명령과 일치하게 기록한다.
```bash
rg --sort path -n "Application\\.instance\\.(property|commonData|commandStates|dataCommandMap)" lib/oto/core lib/oto/commands/command.dart
```
예상: 결과 없음.
```bash
awk '/^## 검증 결과$/{flag=1} /^---$/{if(flag) exit} flag' agent-task/01+02_runtime_context/CODE_REVIEW-cloud-G07.md | rg --sort path -n "^\\(output\\)$|\\.\\.\\. \\(truncated to relevant lines\\)"
```
예상: 결과 없음.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,151 @@
<!-- task=01+03_command_io_ports plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+03_command_io_ports, plan=0, tag=REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+03_command_io_ports/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+03_command_io_ports/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] `CommandRuntime` pilot을 추가하고 Shell/Git/ProcessRun 실행을 위임한다 | [x] |
## 구현 체크리스트
- [x] [REFACTOR-1] `CommandRuntime` pilot을 추가하고 Shell/Git/ProcessRun 실행을 위임한다.
- [x] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [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_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 `agent-task/01+03_command_io_ports/`를 `agent-task/archive/YYYY/MM/01+03_command_io_ports/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- 계획의 `CommandRuntime` 시그니처는 `run`이 `Future<ProcessData>`를 반환한다고 명시했으나, 기존 `ProcessExecutor.run`은 `ProcessResult`를 반환하며 `GitStashPush`/`GitStashApply`도 `ProcessResult.exitCode`(int)와 `.stdout`을 직접 사용한다. 호출부 의미를 바꾸지 않기 위해 `run`의 반환 타입을 `Future<ProcessResult>`로 유지했다.
- 계획의 `start` 시그니처는 `{LogHandler? logHandler}`만 명시했으나, `GitCount`/`GitRev`/`GitReset`/`GitStashApply`가 `printStderr: false`를 그대로 사용하므로 `printStderr` 매개변수를 함께 노출했다.
- 테스트에서 외부 IO 없이 `completeProcess`를 검증하기 위해, `Command.completeProcess`가 `processData.exitCode` 필드를 먼저 보고 없을 때만 `processData.process.exitCode`를 await하도록 좁은 범위로 수정했다. 실제 `ProcessExecutor.start`는 반환 전 `waitForExit`을 수행하므로 동작 의미는 동일하다.
- `lib/oto/commands/git/git_hub.dart`의 `ProcessExecutor.start` 호출(4건)은 GitHub 외부 통합 명령으로, 계획의 pilot 범위(Shell/Git/ProcessRun)에 포함되지 않아 그대로 두었다. 최종 rg 검증 결과에 노출되므로 의도적 예외로 기록한다.
## 주요 설계 결정
- `CommandRuntime`을 `lib/oto/commands/command_runtime.dart`에 도입하고, `Command` 기반 클래스에 `static defaultRuntime`과 인스턴스별 setter를 두어 테스트에서 명령 인스턴스마다 fake runtime을 주입할 수 있게 했다.
- Fake runtime은 `ProcessData` 서브클래스(`FakeProcessData`)로 `process` 게터 호출을 막아 실제 `Process` 핸들 없이도 `completeProcess`가 동작하도록 했다. `waitForExit`은 즉시 완료한다.
- `ProcessRun.startDetached`는 임시 파일 생성·detached spawn을 모두 runtime으로 위임해, `process.dart`에서 직접 `Process.start`/`File` 호출을 제거했다.
- `Copy` 등 파일 IO는 계획대로 손대지 않았다.
## 리뷰어를 위한 체크포인트
- fake runtime 테스트가 실제 process를 시작하지 않는지 확인한다.
- pilot 파일에서 직접 `ProcessExecutor` 호출이 제거되었는지 확인한다.
- `Copy`의 실제 파일 copy 추상화가 범위 밖으로 남아있는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REFACTOR-1 중간 검증
```
$ dart test test/oto_command_runtime_test.dart
00:00 +0: loading test/oto_command_runtime_test.dart
00:00 +0: shell command delegates process start to runtime
00:00 +1: shell command delegates process start to runtime
00:00 +1: git command delegates command buffer to runtime
00:00 +1: git command delegates command buffer to runtime
/tmp/oto_runtime_test
00:00 +2: git command delegates command buffer to runtime
00:00 +2: process run delegates detached start to runtime
00:00 +2: process run delegates detached start to runtime
Process Start: my_server
00:00 +3: process run delegates detached start to runtime
00:00 +3: All tests passed!
```
### 최종 검증
```
$ dart analyze
Analyzing oto...
No issues found!
$ dart test
... (전체 테스트 통과, 마지막 라인)
00:00 +12: All tests passed!
$ rg --sort path -n "ProcessExecutor\.(start|run)|Process\.start" lib/oto/commands/shell lib/oto/commands/git lib/oto/commands/process
lib/oto/commands/git/git_hub.dart:24: await ProcessExecutor.start(shell, logHandler: Application.logWithType);
lib/oto/commands/git/git_hub.dart:40: await ProcessExecutor.start(shell, logHandler: Application.logWithType);
lib/oto/commands/git/git_hub.dart:81: var process = await ProcessExecutor.start(shell,
lib/oto/commands/git/git_hub.dart:120: var process = await ProcessExecutor.start(shell,
```
`git_hub.dart`의 4건은 GitHub 외부 통합 명령으로 pilot 범위 밖이며 `계획 대비 변경 사항`의 의도적 예외 항목과 일치한다. pilot 대상 파일(`shell.dart`, `git.dart`, `process.dart`)에는 직접 호출이 없다.
---
> **[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 the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 종합 판정
PASS
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | Shell/Git/ProcessRun 실행 경로가 `CommandRuntime`으로 위임되고 기존 완료 처리 의미가 유지된다. |
| Completeness | Pass | 계획 및 review stub의 구현 체크리스트가 일치하고 모든 구현 소유 항목이 완료되었다. |
| Test coverage | Pass | `test/oto_command_runtime_test.dart`가 fake runtime으로 Shell/Git/ProcessRun 위임을 검증한다. |
| API contract | Pass | 기본 runtime은 기존 `ProcessExecutor`/detached 실행 계약을 보존하며 주입 가능한 runtime을 제공한다. |
| Code quality | Pass | 불필요한 debug/TODO/범위 외 리팩터링이 없다. |
| Plan deviation | Pass | 반환 타입과 `printStderr` 노출, GitHub 예외가 review stub에 사유와 함께 기록되어 있다. |
| Verification trust | Pass | `dart analyze`, `dart test`, `rg` 검증을 재실행했고 기록과 일치한다. |
### 발견된 문제
없음
### 다음 단계
PASS: `complete.log` 작성 후 `agent-task/archive/2026/05/01+03_command_io_ports/`로 이동한다.

View file

@ -0,0 +1,36 @@
# Complete - 01+03_command_io_ports
## 완료 일시
2026-05-19
## 요약
CommandRuntime pilot으로 Shell/Git/ProcessRun 실행 위임을 도입했고, 1회 plan-review loop가 PASS로 종료되었다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | PASS | fake runtime 테스트와 전체 검증이 통과했다. |
## 구현/정리 내용
- `CommandRuntime`/`DefaultCommandRuntime`을 추가하고 `Command`에서 기본 runtime 및 인스턴스별 runtime 주입을 지원했다.
- Shell, Git, ProcessRun의 process 실행을 runtime으로 위임했다.
- fake runtime 기반 단위 테스트로 Shell/Git/ProcessRun 위임 동작을 검증했다.
## 최종 검증
- `dart analyze` - PASS; `No issues found!`
- `dart test test/oto_command_runtime_test.dart` - PASS; `00:00 +3: All tests passed!`
- `dart test` - PASS; `00:00 +12: All tests passed!`
- `rg --sort path -n "ProcessExecutor\\.(start|run)|Process\\.start" lib/oto/commands/shell lib/oto/commands/git lib/oto/commands/process` - PASS; pilot 대상에는 직접 호출이 없고 `git_hub.dart`의 범위 외 예외 4건만 남았다.
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,222 @@
<!-- task=01+04_command_catalog plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+04_command_catalog, plan=0, tag=REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G06.md` → `code_review_cloud_G06_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-local-G06.md` → `plan_local_G06_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] `CommandSpec` 카탈로그를 추가하고 등록 누락 검증을 만든다 | [x] |
## 구현 체크리스트
- [x] [REFACTOR-1] `CommandSpec` 카탈로그를 추가하고 등록 누락 검증을 만든다.
- [x] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [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_G06_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 `agent-task/01+04_command_catalog/`를 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] WARN/FAIL이면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-cloud-G06.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- 계획에는 명시되지 않았으나 spec 누락 외에 `category`/`dataModel`이 빈 문자열인 경우도 잡도록 4번째 테스트 `spec category and dataModel are non-empty`를 추가했다. AI가 무심코 빈 문자열로 등록하는 회귀를 막기 위함이며 추가 비용은 없다.
- 테스트 외부에서도 `CommandType.values` 누락을 점검할 수 있도록 `Command.registeredTypes` 공개 getter를 추가했다. 계획의 `Command.specs` getter와 같은 결의 최소 노출이다.
## 주요 설계 결정
- `CommandSpec`은 `command.dart`에 둔다. `Command.register()`가 이미 그 파일에 있고, spec과 register는 한 호출에서 함께 다뤄야 부분 등록을 방지할 수 있다.
- `Command.register(type, factory, {CommandSpec? spec})`로 optional 파라미터를 채택했다. 기존 호출 시그니처와 호환되므로 점진적 적용이 가능하지만, 본 작업에서는 모든 호출 사이트를 한 번에 spec과 함께 등록했다.
- `spec.type == type` 일치 검증은 `assert`로 두어 prod 영향은 없지만 dev/test에서 잘못된 매핑을 잡는다.
- `dataModel` 필드 값은 데이터 클래스 이름(`DataGit`, `DataBuildiOS` …)을 사용한다. AI가 grep으로 클래스 정의와 직렬화 사용처를 즉시 찾을 수 있고, 파일 경로보다 더 정밀한 식별자이기 때문이다.
- `samplePath`는 해당 command가 실제로 등장하는 샘플 파일만 지정했다. 실제 grep으로 검증한 매핑이며, sample 도메인 rule(`agent-ops/rules/project/domain/sample/rules.md`)의 키워드 표와 일치한다. 샘플이 없는 command(`Jira`, `GitHub*`, `Docker`, `Gradle`, `AwsCli`, `SMBAuth`, `JenkinsParameterModify`, `Protobuf`, `Rename`, `Files`, `URLInfo`, `FileDiffCheck`, `DirectoryCreate`, `GitPull/Reset/Stash*`, `CodeSign*`, `ProductBuild`, `Notarize`, `XcodeprojAddFile`, `CreateAppData`, `BuildDartCompile`, `BuildDotNet`, `BuildMSBuild`, `PublishiOS`, `SlackFile`, `Delay`, `SimpleCommand`)은 `null`로 두어 미존재 경로 false-positive를 피했다.
## 리뷰어를 위한 체크포인트
- `CommandType.values` 전체가 등록되는지 확인한다.
- 모든 등록 command가 spec을 갖는지 확인한다.
- samplePath가 실제 파일만 가리키는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REFACTOR-1 중간 검증
```
$ dart test test/oto_command_catalog_test.dart
00:00 +0: loading test/oto_command_catalog_test.dart
00:00 +0: (setUpAll)
00:00 +0: all command types are registered
00:00 +1: all command types are registered
00:00 +1: all registered commands expose specs
00:00 +2: all registered commands expose specs
00:00 +2: spec sample paths exist when provided
00:00 +3: spec sample paths exist when provided
00:00 +3: spec category and dataModel are non-empty
00:00 +4: spec category and dataModel are non-empty
00:00 +4: (tearDownAll)
00:00 +4: All tests passed!
```
### 최종 검증
```
$ dart analyze
Analyzing oto...
No issues found!
$ dart test
... (전체 test/ suite 실행)
00:00 +16: test/oto_application_test.dart: CommandExe exe -f handles missing file without LateInitializationError
00:00 +16: All tests passed!
$ rg --sort path -n "Command\\.register\\(" lib/oto/commands
lib/oto/commands/aws/awscli.dart:27: Command.register(
lib/oto/commands/build/app_data_creator.dart:25: Command.register(
lib/oto/commands/build/build_dart.dart:39: Command.register(
lib/oto/commands/build/build_dart_compile.dart:60: Command.register(
lib/oto/commands/build/build_dot_net.dart:29: Command.register(
lib/oto/commands/build/build_flutter.dart:28: Command.register(
lib/oto/commands/build/build_ios.dart:574: Command.register(
lib/oto/commands/build/build_ios.dart:584: Command.register(
lib/oto/commands/build/build_ios.dart:594: Command.register(
lib/oto/commands/build/build_ios.dart:604: Command.register(
lib/oto/commands/build/build_ios.dart:613: Command.register(
lib/oto/commands/build/build_ios.dart:622: Command.register(
lib/oto/commands/build/build_ios.dart:631: Command.register(
lib/oto/commands/build/build_msbuild.dart:37: Command.register(
lib/oto/commands/build/publish_ios.dart:56: Command.register(
lib/oto/commands/build/testflight_ios.dart:112: Command.register(
lib/oto/commands/build/testflight_ios.dart:122: Command.register(
lib/oto/commands/build/testflight_ios.dart:132: Command.register(
lib/oto/commands/build/xcodeproj.dart:60: Command.register(
lib/oto/commands/command_registry.dart:42: Command.register(
lib/oto/commands/docker/docker.dart:28: Command.register(
lib/oto/commands/file/copy.dart:81: Command.register(
lib/oto/commands/file/delete.dart:64: Command.register(
lib/oto/commands/file/directory.dart:22: Command.register(
lib/oto/commands/file/file.dart:111: Command.register(
lib/oto/commands/file/file.dart:121: Command.register(
lib/oto/commands/file/file.dart:131: Command.register(
lib/oto/commands/file/file.dart:140: Command.register(
lib/oto/commands/file/file_diff_check.dart:128: Command.register(
lib/oto/commands/file/rename.dart:40: Command.register(
lib/oto/commands/file/zip.dart:81: Command.register(
lib/oto/commands/ftp/download.dart:48: Command.register(
lib/oto/commands/ftp/upload.dart:77: Command.register(
lib/oto/commands/git/git.dart:291: Command.register(
lib/oto/commands/git/git.dart:301: Command.register(
lib/oto/commands/git/git.dart:311: Command.register(
lib/oto/commands/git/git.dart:320: Command.register(
lib/oto/commands/git/git.dart:330: Command.register(
lib/oto/commands/git/git.dart:340: Command.register(
lib/oto/commands/git/git.dart:350: Command.register(
lib/oto/commands/git/git.dart:360: Command.register(
lib/oto/commands/git/git.dart:369: Command.register(
lib/oto/commands/git/git.dart:378: Command.register(
lib/oto/commands/git/git.dart:387: Command.register(
lib/oto/commands/git/git_hub.dart:127: Command.register(
lib/oto/commands/git/git_hub.dart:136: Command.register(
lib/oto/commands/git/git_hub.dart:145: Command.register(
lib/oto/commands/git/git_hub.dart:154: Command.register(
lib/oto/commands/gradle/gradle.dart:27: Command.register(
lib/oto/commands/infra/smb_authorize.dart:28: Command.register(
lib/oto/commands/jenkins/jenkins.dart:68: Command.register(
lib/oto/commands/jira/jira.dart:126: Command.register(
lib/oto/commands/notification/mattermost.dart:100: Command.register(
lib/oto/commands/notification/mattermost.dart:110: Command.register(
lib/oto/commands/notification/slack.dart:82: Command.register(
lib/oto/commands/notification/slack.dart:92: Command.register(
lib/oto/commands/notification/slack.dart:101: Command.register(
lib/oto/commands/process/process.dart:56: Command.register(
lib/oto/commands/process/process.dart:66: Command.register(
lib/oto/commands/process/process.dart:76: Command.register(
lib/oto/commands/proto/protobuf.dart:26: Command.register(
lib/oto/commands/shell/shell.dart:57: Command.register(
lib/oto/commands/shell/shell.dart:67: Command.register(
lib/oto/commands/util/json.dart:139: Command.register(
lib/oto/commands/util/json.dart:149: Command.register(
lib/oto/commands/util/json.dart:159: Command.register(
lib/oto/commands/util/print.dart:20: Command.register(
lib/oto/commands/util/set_value.dart:90: Command.register(
lib/oto/commands/util/string_util.dart:118: Command.register(
lib/oto/commands/util/string_util.dart:128: Command.register(
lib/oto/commands/util/string_util.dart:138: Command.register(
lib/oto/commands/util/string_util.dart:148: Command.register(
lib/oto/commands/util/string_util.dart:158: Command.register(
lib/oto/commands/util/timer.dart:37: Command.register(
lib/oto/commands/web/web.dart:85: Command.register(
lib/oto/commands/web/web.dart:95: Command.register(
```
(총 76개 호출 — CommandType.values 76개와 일치, 모두 spec 동반)
---
> **[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 the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 종합 판정
FAIL
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Fail | `BuildDart`의 `samplePath`가 실제 `BuildDart` 예시가 없는 샘플을 가리킨다. |
| Completeness | Pass | 계획된 `CommandSpec` 도입, 등록 누락 검증, 구현 기록 작성은 완료되었다. |
| Test coverage | Fail | 현재 samplePath 테스트는 파일 존재만 검증해 잘못된 command/sample 매핑을 통과시킨다. |
| API contract | Pass | 기존 `Command.register(type, factory)` 호출은 optional named parameter로 호환된다. |
| Code quality | Pass | 확인 범위에서 디버그 출력, 죽은 코드, 불필요한 구조 변경은 보이지 않는다. |
| Plan deviation | Pass | 빈 category/dataModel 검증 추가는 계획 대비 정당한 강화다. |
| Verification trust | Pass | `dart test test/oto_command_catalog_test.dart`, `dart analyze`, `dart test`, `rg --sort path -n "Command\\.register\\(" lib/oto/commands`를 재실행해 기록된 성공 상태와 맞음을 확인했다. |
### 발견된 문제
- Required: `lib/oto/commands/build/build_dart.dart:46`의 `BuildDart` spec이 `assets/yaml/sample/11_build_flutter.yaml`를 가리키지만, 해당 파일에는 `BuildDart` 명령이 없고 `BuildFlutter`만 있다(`assets/yaml/sample/11_build_flutter.yaml:36`, `assets/yaml/sample/11_build_flutter.yaml:44`). 실제 `BuildDart` 예시는 `assets/yaml/sample/12_scheduler.yaml:33`에 있다. `samplePath`를 실제 예시 파일로 바꾸거나 `null`로 두고, `test/oto_command_catalog_test.dart:39`의 samplePath 검증을 “파일 존재 + 주석이 아닌 `- command: <CommandType>` 포함”까지 확인하도록 강화해야 한다.
### 다음 단계
FAIL: 위 Required 이슈를 해결하는 후속 plan/review 파일을 작성한다.

View file

@ -0,0 +1,146 @@
<!-- task=01+04_command_catalog plan=1 tag=REVIEW_REFACTOR -->
# Code Review Reference - REVIEW_REFACTOR
> **[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.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-19
task=01+04_command_catalog, plan=1, tag=REVIEW_REFACTOR
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
Review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G06.md` → `code_review_cloud_G06_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-local-G06.md` → `plan_local_G06_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REFACTOR-1] `BuildDart` samplePath를 실제 예시와 맞추고 samplePath 검증을 강화한다 | [x] |
## 구현 체크리스트
- [x] [REVIEW_REFACTOR-1] `BuildDart` samplePath를 실제 예시와 맞추고 samplePath 검증을 강화한다.
- [x] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [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_G06_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 `agent-task/01+04_command_catalog/`를 `agent-task/archive/YYYY/MM/01+04_command_catalog/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-cloud-G06.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- 계획에서는 기존 `spec sample paths exist when provided` 테스트를 확장하거나 별도 테스트를 추가하는 두 옵션을 제시했다. 별도 테스트(`spec sample paths contain the registered command`)로 분리하여 "파일 존재"와 "command 포함" 실패 원인이 명확히 구분되도록 했다. 진단 reason도 각각 다르다.
## 주요 설계 결정
- `BuildDart` samplePath를 `assets/yaml/sample/12_scheduler.yaml`로 변경했다. `11_build_flutter.yaml`에는 `BuildFlutter`만 있고, `12_scheduler.yaml:33`에 실제 `- command: BuildDart` 라인이 있다.
- 새 검증은 `entry.key.name` (Dart 2.15+ enum name)으로 command 이름을 얻고, `^\s*-\s*command:\s*<Name>\s*$` 정규식으로 매칭한다. `line.trimLeft().startsWith('#')`인 줄은 주석 처리된 예시(`# - command: SlackFile`)로 보고 제외한다.
- 파일 존재 검증과 command 포함 검증을 분리한 이유: 파일 자체가 없으면 첫 테스트가 명확히 실패하고, command가 없으면 두 번째 테스트가 잘못된 매핑을 가리킨다. 한 테스트에서 두 실패 모드를 섞으면 reason이 모호해진다.
## 리뷰어를 위한 체크포인트
- `BuildDart`의 `samplePath`가 실제 `BuildDart` command가 있는 파일을 가리키는지 확인한다.
- samplePath 검증 테스트가 파일 존재뿐 아니라 해당 command 포함 여부까지 확인하는지 확인한다.
- 주석 처리된 command 예시가 samplePath 검증을 통과시키지 않는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REVIEW_REFACTOR-1 중간 검증
```
$ dart test test/oto_command_catalog_test.dart
00:00 +0: loading test/oto_command_catalog_test.dart
00:00 +0: (setUpAll)
00:00 +0: all command types are registered
00:00 +1: all command types are registered
00:00 +1: all registered commands expose specs
00:00 +2: all registered commands expose specs
00:00 +2: spec sample paths exist when provided
00:00 +3: spec sample paths exist when provided
00:00 +3: spec sample paths contain the registered command
00:00 +4: spec sample paths contain the registered command
00:00 +4: spec category and dataModel are non-empty
00:00 +5: spec category and dataModel are non-empty
00:00 +5: (tearDownAll)
00:00 +5: All tests passed!
```
### 최종 검증
```
$ dart analyze
Analyzing oto...
No issues found!
$ dart test
... (전체 test/ suite 실행)
00:00 +17: test/oto_application_test.dart: CommandExe exe -f handles missing file without LateInitializationError
00:00 +17: All tests passed!
$ rg --sort path -n "command:\s+BuildDart\b" assets/yaml/sample
assets/yaml/sample/12_scheduler.yaml:33:- command: BuildDart
```
(BuildDart samplePath가 가리키는 `assets/yaml/sample/12_scheduler.yaml`에 실제 `- command: BuildDart` 라인이 존재함을 확인)
---
> **[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 the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 종합 판정
PASS
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | `BuildDart`의 `samplePath`가 실제 `BuildDart` 예시가 있는 `assets/yaml/sample/12_scheduler.yaml`를 가리킨다. |
| Completeness | Pass | follow-up 계획의 구현 항목, 검증 실행, review 파일 기록이 모두 완료되었다. |
| Test coverage | Pass | samplePath 파일 존재 검증에 더해 해당 파일에 주석이 아닌 `- command: <Type>` 라인이 있는지 검증한다. |
| API contract | Pass | public API 변경 없이 metadata 값과 테스트만 보강했다. |
| Code quality | Pass | 수정 범위가 좁고, 불필요한 디버그 출력이나 dead code가 없다. |
| Plan deviation | Pass | 계획에서 허용한 별도 테스트 추가 방식으로 구현했고, 변경 사유가 기록되어 있다. |
| Verification trust | Pass | `dart test test/oto_command_catalog_test.dart`, `dart analyze`, `dart test`, `rg --sort path -n "command:\\s+BuildDart\\b" assets/yaml/sample`를 재실행해 기록과 현재 코드가 일치함을 확인했다. |
### 발견된 문제
없음
### 다음 단계
PASS: `complete.log` 작성 후 task 디렉터리를 `agent-task/archive/2026/05/01+04_command_catalog/`로 이동한다.

View file

@ -0,0 +1,38 @@
# Complete - 01+04_command_catalog
## 완료 일시
2026-05-19
## 요약
CommandSpec 카탈로그와 등록 누락 검증을 추가하고, 2회 리뷰 루프 끝에 PASS로 종료했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G06_0.log` | `code_review_cloud_G06_0.log` | FAIL | `BuildDart` samplePath가 실제 `BuildDart` 예시가 없는 샘플을 가리키고, 테스트가 파일 존재만 검증하는 문제가 발견되었다. |
| `plan_local_G06_1.log` | `code_review_cloud_G06_1.log` | PASS | `BuildDart` samplePath를 실제 예시 파일로 맞추고, samplePath가 해당 command를 포함하는지 검증하도록 테스트를 보강했다. |
## 구현/정리 내용
- `CommandSpec` 카탈로그와 `Command.specs`, `Command.registeredTypes` 노출을 추가했다.
- 모든 `Command.register()` 호출에 command category, dataModel, samplePath metadata를 연결했다.
- `test/oto_command_catalog_test.dart`에 command 등록 누락, spec 누락, samplePath 존재, samplePath command 포함, category/dataModel 빈 값 검증을 추가했다.
- `BuildDart` samplePath를 `assets/yaml/sample/12_scheduler.yaml`로 수정했다.
## 최종 검증
- `dart test test/oto_command_catalog_test.dart` - PASS; 5개 command catalog 테스트가 모두 통과했다.
- `dart analyze` - PASS; `No issues found!`.
- `dart test` - PASS; 전체 테스트 17개가 모두 통과했다.
- `rg --sort path -n "command:\\s+BuildDart\\b" assets/yaml/sample` - PASS; `assets/yaml/sample/12_scheduler.yaml:33:- command: BuildDart`.
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,137 @@
<!-- task=01+04_command_catalog plan=1 tag=REVIEW_REFACTOR -->
# Command Catalog Follow-up Plan
## 이 파일을 읽는 구현 에이전트에게
**필수 경고: 구현 마지막 단계에서 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 모두 채워야 한다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
구현 체크리스트와 검증 명령을 그대로 수행하고 실제 출력을 review stub에 기록한다.
review 파일의 아카이브 지시와 `코드리뷰 전용 체크리스트`는 리뷰 에이전트 전용이다.
## 배경
1차 리뷰에서 `BuildDart`의 `CommandSpec.samplePath`가 실제 `BuildDart` 예시가 없는 `assets/yaml/sample/11_build_flutter.yaml`를 가리키는 문제가 발견되었다.
현재 테스트는 samplePath 파일 존재만 확인하므로 이 잘못된 매핑을 통과시킨다.
## 분석 결과
### 읽은 파일
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/sample/rules.md`
- `agent-task/01+04_command_catalog/plan_local_G06_0.log`
- `agent-task/01+04_command_catalog/code_review_cloud_G06_0.log`
- `lib/oto/commands/build/build_dart.dart`
- `test/oto_command_catalog_test.dart`
- `assets/yaml/sample/11_build_flutter.yaml`
- `assets/yaml/sample/12_scheduler.yaml`
### 리뷰 발견 사항
- `lib/oto/commands/build/build_dart.dart:46`의 `samplePath`는 `assets/yaml/sample/11_build_flutter.yaml`이다.
- `assets/yaml/sample/11_build_flutter.yaml`에는 `BuildFlutter`만 있고 `BuildDart` 명령은 없다.
- `assets/yaml/sample/12_scheduler.yaml:33`에는 실제 `BuildDart` 명령 예시가 있다.
- `test/oto_command_catalog_test.dart`의 `spec sample paths exist when provided` 테스트는 파일 존재만 검증한다.
### 범위 결정 근거
- 이 follow-up은 `BuildDart` samplePath 오표기와 해당 회귀 검증만 다룬다.
- command registry 전체 구조, 데이터 모델명 검증 확장, README 생성은 이번 범위가 아니다.
- 샘플 YAML 내용 자체를 바꾸지 않고 기존 실제 예시를 우선 사용한다.
### 빌드 등급
- build lane: `local-G06` — deterministic metadata/test 수정이며 라이브 환경 의존성이 없다.
- review lane: `cloud-G06` — command catalog의 cross-file metadata 검증을 다시 확인해야 한다.
## 구현 체크리스트
- [ ] [REVIEW_REFACTOR-1] `BuildDart` samplePath를 실제 예시와 맞추고 samplePath 검증을 강화한다.
- [ ] 모든 중간 검증과 최종 검증을 실행하고 실제 출력을 `CODE_REVIEW-*-G??.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- `agent-task/01+04_command_catalog/code_review_cloud_G06_0.log`의 Required 이슈를 해결한다.
- 기존 command catalog 구현 위에서 최소 수정한다.
### [REVIEW_REFACTOR-1] BuildDart samplePath 및 검증 강화
#### 문제
`BuildDart` spec이 `assets/yaml/sample/11_build_flutter.yaml`를 가리키지만, 해당 파일은 `BuildFlutter` 예시만 포함한다.
이 상태에서는 AI가 `BuildDart` 사용 예시를 찾을 때 잘못된 샘플로 안내된다.
```dart
// lib/oto/commands/build/build_dart.dart
samplePath: 'assets/yaml/sample/11_build_flutter.yaml',
```
#### 해결 방법
`BuildDart`의 `samplePath`를 실제 `BuildDart` 명령이 있는 `assets/yaml/sample/12_scheduler.yaml`로 바꾸거나, command-specific sample로 보기 어렵다고 판단하면 `null`로 둔다.
테스트는 `samplePath`가 존재할 때 해당 파일 안에 주석이 아닌 `- command: <CommandType>` 라인이 있는지 검증한다.
주석 처리된 예시(`# - command: SlackFile`)는 통과시키지 않는다.
권장 검증 방식:
```dart
final commandName = entry.key.toString().replaceAll('CommandType.', '');
final commandPattern = RegExp(r'^\s*-\s*command:\s*' + RegExp.escape(commandName) + r'\s*$');
final hasCommand = File(sample).readAsLinesSync().any((line) {
final trimmed = line.trimLeft();
return !trimmed.startsWith('#') && commandPattern.hasMatch(line);
});
```
#### 수정 파일 및 체크리스트
- [ ] `lib/oto/commands/build/build_dart.dart`의 `BuildDart` samplePath를 실제 예시와 맞춘다.
- [ ] `test/oto_command_catalog_test.dart`에서 samplePath 파일이 해당 command를 포함하는지 검증한다.
- [ ] 주석 처리된 command 라인은 samplePath 검증에서 제외한다.
- [ ] `dart test test/oto_command_catalog_test.dart`로 회귀 테스트를 확인한다.
#### 테스트 작성
- 수정: `test/oto_command_catalog_test.dart`
- 테스트명: 기존 `spec sample paths exist when provided`를 확장하거나 `spec sample paths contain the registered command`를 추가한다.
- 목적: samplePath가 존재하는 파일이더라도 다른 command의 샘플을 잘못 가리키는 회귀를 잡는다.
#### 중간 검증
```bash
dart test test/oto_command_catalog_test.dart
```
예상: command catalog 테스트 통과.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `lib/oto/commands/build/build_dart.dart` | REVIEW_REFACTOR-1 |
| `test/oto_command_catalog_test.dart` | REVIEW_REFACTOR-1 |
## 최종 검증
```bash
dart analyze
```
예상: `No issues found!`
```bash
dart test
```
예상: 모든 테스트 통과.
```bash
rg --sort path -n "command:\\s+BuildDart\\b" assets/yaml/sample
```
예상: `BuildDart` samplePath가 가리키는 파일에 실제 `BuildDart` command가 존재한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -12,6 +12,7 @@ import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_registry.dart'; import 'package:oto/oto/commands/command_registry.dart';
import 'package:oto/oto/core/build_result.dart'; import 'package:oto/oto/core/build_result.dart';
import 'package:oto/oto/core/defined_data.dart'; import 'package:oto/oto/core/defined_data.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/pipeline/pipeline.dart'; import 'package:oto/oto/pipeline/pipeline.dart';
import 'package:oto/oto/data/command_data.dart'; import 'package:oto/oto/data/command_data.dart';
import 'package:oto/oto/core/data_composer.dart'; import 'package:oto/oto/core/data_composer.dart';
@ -46,10 +47,22 @@ class Application {
BuildType.file: () => DataComposerFile() BuildType.file: () => DataComposerFile()
}; };
DataCommon? commonData; ExecutionContext context = ExecutionContext();
Map<String, dynamic> property = {};
Map<String, CommandState> commandStates = {}; DataCommon? get commonData => context.commonData;
Map<String, DataCommand> dataCommandMap = {}; set commonData(DataCommon? value) => context.commonData = value;
Map<String, dynamic> get property => context.property;
set property(Map<String, dynamic> value) => context.property = value;
Map<String, CommandState> get commandStates => context.commandStates;
set commandStates(Map<String, CommandState> value) =>
context.commandStates = value;
Map<String, DataCommand> get dataCommandMap => context.dataCommandMap;
set dataCommandMap(Map<String, DataCommand> value) =>
context.dataCommandMap = value;
late Pipeline pipeline; late Pipeline pipeline;
late BuildType _buildType; late BuildType _buildType;

View file

@ -24,5 +24,13 @@ class AwsCli extends Command {
} }
void registerAwsCliCommand() { void registerAwsCliCommand() {
Command.register(CommandType.AwsCli, () => AwsCli()); Command.register(
CommandType.AwsCli,
() => AwsCli(),
spec: const CommandSpec(
type: CommandType.AwsCli,
category: 'aws',
dataModel: 'DataAwsCli',
),
);
} }

View file

@ -22,5 +22,13 @@ class CreateAppData extends Command {
} }
void registerAppDataCreatorCommands() { void registerAppDataCreatorCommands() {
Command.register(CommandType.CreateAppData, () => CreateAppData()); Command.register(
CommandType.CreateAppData,
() => CreateAppData(),
spec: const CommandSpec(
type: CommandType.CreateAppData,
category: 'build',
dataModel: 'DataAppDataCreator',
),
);
} }

View file

@ -36,5 +36,14 @@ class BuildDart extends Command {
} }
void registerBuildDartCommands() { void registerBuildDartCommands() {
Command.register(CommandType.BuildDart, () => BuildDart()); Command.register(
CommandType.BuildDart,
() => BuildDart(),
spec: const CommandSpec(
type: CommandType.BuildDart,
category: 'build',
dataModel: 'DataBuildDart',
samplePath: 'assets/yaml/sample/12_scheduler.yaml',
),
);
} }

View file

@ -57,5 +57,13 @@ class BuildDartCompile extends BuildDart {
} }
void registerBuildDartCompileCommands() { void registerBuildDartCompileCommands() {
Command.register(CommandType.BuildDartCompile, () => BuildDartCompile()); Command.register(
CommandType.BuildDartCompile,
() => BuildDartCompile(),
spec: const CommandSpec(
type: CommandType.BuildDartCompile,
category: 'build',
dataModel: 'DataBuildDartCompile',
),
);
} }

View file

@ -26,5 +26,13 @@ class BuildDotNet extends Command {
} }
void registerBuildDotNetCommands() { void registerBuildDotNetCommands() {
Command.register(CommandType.BuildDotNet, () => BuildDotNet()); Command.register(
CommandType.BuildDotNet,
() => BuildDotNet(),
spec: const CommandSpec(
type: CommandType.BuildDotNet,
category: 'build',
dataModel: 'DataBuildDotNet',
),
);
} }

View file

@ -25,5 +25,14 @@ class BuildFlutter extends BuildDart {
} }
void registerBuildFlutterCommands() { void registerBuildFlutterCommands() {
Command.register(CommandType.BuildFlutter, () => BuildFlutter()); Command.register(
CommandType.BuildFlutter,
() => BuildFlutter(),
spec: const CommandSpec(
type: CommandType.BuildFlutter,
category: 'build',
dataModel: 'DataBuildFlutter',
samplePath: 'assets/yaml/sample/11_build_flutter.yaml',
),
);
} }

View file

@ -571,11 +571,70 @@ class ExportiOS extends Command {
} }
void registerBuildIosCommands() { void registerBuildIosCommands() {
Command.register(CommandType.BuildiOS, () => BuildiOS()); Command.register(
Command.register(CommandType.ArchiveiOS, () => ArchiveiOS()); CommandType.BuildiOS,
Command.register(CommandType.ExportiOS, () => ExportiOS()); () => BuildiOS(),
Command.register(CommandType.CodeSign, () => CodeSign()); spec: const CommandSpec(
Command.register(CommandType.CodeSignVerify, () => CodeSignVerify()); type: CommandType.BuildiOS,
Command.register(CommandType.ProductBuild, () => ProductBuild()); category: 'build',
Command.register(CommandType.Notarize, () => Notarize()); dataModel: 'DataBuildiOS',
samplePath: 'assets/yaml/sample/10_build_ios.yaml',
),
);
Command.register(
CommandType.ArchiveiOS,
() => ArchiveiOS(),
spec: const CommandSpec(
type: CommandType.ArchiveiOS,
category: 'build',
dataModel: 'DataArchiveiOS',
samplePath: 'assets/yaml/sample/10_build_ios.yaml',
),
);
Command.register(
CommandType.ExportiOS,
() => ExportiOS(),
spec: const CommandSpec(
type: CommandType.ExportiOS,
category: 'build',
dataModel: 'DataExportiOS',
samplePath: 'assets/yaml/sample/10_build_ios.yaml',
),
);
Command.register(
CommandType.CodeSign,
() => CodeSign(),
spec: const CommandSpec(
type: CommandType.CodeSign,
category: 'build',
dataModel: 'DataCodeSign',
),
);
Command.register(
CommandType.CodeSignVerify,
() => CodeSignVerify(),
spec: const CommandSpec(
type: CommandType.CodeSignVerify,
category: 'build',
dataModel: 'DataCodeSignVerify',
),
);
Command.register(
CommandType.ProductBuild,
() => ProductBuild(),
spec: const CommandSpec(
type: CommandType.ProductBuild,
category: 'build',
dataModel: 'DataProductBuild',
),
);
Command.register(
CommandType.Notarize,
() => Notarize(),
spec: const CommandSpec(
type: CommandType.Notarize,
category: 'build',
dataModel: 'DataNotarize',
),
);
} }

View file

@ -34,5 +34,13 @@ class BuildMSBuild extends Command {
} }
void registerBuildMsbuildCommands() { void registerBuildMsbuildCommands() {
Command.register(CommandType.BuildMSBuild, () => BuildMSBuild()); Command.register(
CommandType.BuildMSBuild,
() => BuildMSBuild(),
spec: const CommandSpec(
type: CommandType.BuildMSBuild,
category: 'build',
dataModel: 'DataBuildMSBuild',
),
);
} }

View file

@ -53,5 +53,13 @@ class PublishiOS extends Command {
} }
void registerPublishIosCommands() { void registerPublishIosCommands() {
Command.register(CommandType.PublishiOS, () => PublishiOS()); Command.register(
CommandType.PublishiOS,
() => PublishiOS(),
spec: const CommandSpec(
type: CommandType.PublishiOS,
category: 'build',
dataModel: 'DataPublishiOS',
),
);
} }

View file

@ -109,7 +109,34 @@ class TestflightDistribute extends Command {
} }
void registerTestflightCommands() { void registerTestflightCommands() {
Command.register(CommandType.TestflightUpload, () => TestflightUpload()); Command.register(
Command.register(CommandType.TestflightStatusCheck, () => TestflightStatusCheck()); CommandType.TestflightUpload,
Command.register(CommandType.TestflightDistribute, () => TestflightDistribute()); () => TestflightUpload(),
spec: const CommandSpec(
type: CommandType.TestflightUpload,
category: 'build',
dataModel: 'DataTestflightUpload',
samplePath: 'assets/yaml/sample/10_build_ios.yaml',
),
);
Command.register(
CommandType.TestflightStatusCheck,
() => TestflightStatusCheck(),
spec: const CommandSpec(
type: CommandType.TestflightStatusCheck,
category: 'build',
dataModel: 'DataTestflightStatusCheck',
samplePath: 'assets/yaml/sample/10_build_ios.yaml',
),
);
Command.register(
CommandType.TestflightDistribute,
() => TestflightDistribute(),
spec: const CommandSpec(
type: CommandType.TestflightDistribute,
category: 'build',
dataModel: 'DataTestflightDistribute',
samplePath: 'assets/yaml/sample/10_build_ios.yaml',
),
);
} }

View file

@ -57,5 +57,13 @@ puts "✅ {ADD_FILE_PATH} has been added to the project and included in Copy Bun
} }
void registerXcodeprojCommands() { void registerXcodeprojCommands() {
Command.register(CommandType.XcodeprojAddFile, () => XcodeprojAddFile()); Command.register(
CommandType.XcodeprojAddFile,
() => XcodeprojAddFile(),
spec: const CommandSpec(
type: CommandType.XcodeprojAddFile,
category: 'build',
dataModel: 'DataXcodeprojAddFile',
),
);
} }

View file

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:dart_framework/platform/process.dart'; import 'package:dart_framework/platform/process.dart';
import 'package:dart_framework/utils/system_util.dart'; import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/oto/application.dart'; import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command_runtime.dart';
import 'package:oto/oto/core/tag_system.dart'; import 'package:oto/oto/core/tag_system.dart';
import 'package:oto/oto/data/command_data.dart'; import 'package:oto/oto/data/command_data.dart';
import 'package:oto/oto/pipeline/pipeline_exe_handle.dart'; import 'package:oto/oto/pipeline/pipeline_exe_handle.dart';
@ -88,13 +89,42 @@ enum CommandType {
JenkinsParameterModify JenkinsParameterModify
} }
class CommandSpec {
final CommandType type;
final String category;
final String dataModel;
final String? samplePath;
const CommandSpec({
required this.type,
required this.category,
required this.dataModel,
this.samplePath,
});
}
abstract class Command { abstract class Command {
static final Map<CommandType, Command Function()> _commandMap = {}; static final Map<CommandType, Command Function()> _commandMap = {};
static final Map<CommandType, CommandSpec> _specMap = {};
static void register(CommandType type, Command Function() factory) { static void register(
CommandType type,
Command Function() factory, {
CommandSpec? spec,
}) {
_commandMap[type] = factory; _commandMap[type] = factory;
if (spec != null) {
assert(spec.type == type,
'CommandSpec.type (${spec.type}) must match registered type ($type)');
_specMap[type] = spec;
}
} }
static Map<CommandType, CommandSpec> get specs =>
Map.unmodifiable(_specMap);
static Iterable<CommandType> get registeredTypes => _commandMap.keys;
factory Command.byType(CommandType type) { factory Command.byType(CommandType type) {
final factory = _commandMap[type]; final factory = _commandMap[type];
if (factory == null) { if (factory == null) {
@ -132,17 +162,22 @@ abstract class Command {
late ExeHandleData? exeHandle = null; late ExeHandleData? exeHandle = null;
static CommandRuntime defaultRuntime = const DefaultCommandRuntime();
CommandRuntime? _runtime;
CommandRuntime get runtime => _runtime ?? defaultRuntime;
set runtime(CommandRuntime value) => _runtime = value;
late String _workspace; late String _workspace;
String get workspace { String get workspace {
return _workspace; return _workspace;
} }
Map<String, dynamic> get property { Map<String, dynamic> get property {
return Application.instance.property; return Application.instance.context.property;
} }
DataCommon? get commonData { DataCommon? get commonData {
return Application.instance.commonData; return Application.instance.context.commonData;
} }
String getWorkspace(String? dataWorkspace) { String getWorkspace(String? dataWorkspace) {
@ -223,7 +258,7 @@ abstract class Command {
Future completeProcess(ProcessData processData, DataCommand command, Future completeProcess(ProcessData processData, DataCommand command,
{List<int>? passExitCodes}) async { {List<int>? passExitCodes}) async {
var exitCode = await processData.process.exitCode; var exitCode = processData.exitCode ?? await processData.process.exitCode;
var passExitCodesMerge = var passExitCodesMerge =
await getMergedPassExitCode(command.param, passExitCodes); await getMergedPassExitCode(command.param, passExitCodes);
String? resultMessage = null; String? resultMessage = null;

View file

@ -39,7 +39,15 @@ import 'package:oto/oto/commands/util/timer.dart';
import 'package:oto/oto/commands/web/web.dart'; import 'package:oto/oto/commands/web/web.dart';
void registerAllCommands() { void registerAllCommands() {
Command.register(CommandType.SimpleCommand, () => SimpleCommand()); Command.register(
CommandType.SimpleCommand,
() => SimpleCommand(),
spec: const CommandSpec(
type: CommandType.SimpleCommand,
category: 'util',
dataModel: 'DataSimpleCommand',
),
);
// build // build
registerBuildIosCommands(); registerBuildIosCommands();

View file

@ -0,0 +1,43 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:async';
import 'dart:io';
import 'package:dart_framework/platform/process.dart';
import 'package:oto/oto/application.dart';
abstract class CommandRuntime {
Future<ProcessData> start(StringBuffer shell,
{bool printStderr = true, LogHandler? logHandler});
Future<ProcessResult> run(StringBuffer shell);
Future<void> startDetached(StringBuffer shell, {required String workspace});
}
class DefaultCommandRuntime implements CommandRuntime {
const DefaultCommandRuntime();
@override
Future<ProcessData> start(StringBuffer shell,
{bool printStderr = true, LogHandler? logHandler}) {
return ProcessExecutor.start(shell,
printStderr: printStderr,
logHandler: logHandler ?? Application.logWithType);
}
@override
Future<ProcessResult> run(StringBuffer shell) => ProcessExecutor.run(shell);
@override
Future<void> startDetached(StringBuffer shell,
{required String workspace}) async {
final file =
File('${Directory.systemTemp.path}/${ProcessExecutor.tempFileName}');
await file.writeAsString(shell.toString());
await Process.start(file.path, [],
runInShell: true,
workingDirectory: workspace,
mode: ProcessStartMode.detached);
}
}

View file

@ -25,5 +25,13 @@ class Docker extends Command {
} }
void registerDockerCommand() { void registerDockerCommand() {
Command.register(CommandType.Docker, () => Docker()); Command.register(
CommandType.Docker,
() => Docker(),
spec: const CommandSpec(
type: CommandType.Docker,
category: 'docker',
dataModel: 'DataDocker',
),
);
} }

View file

@ -78,5 +78,14 @@ class Copy extends Command {
} }
void registerCopyCommand() { void registerCopyCommand() {
Command.register(CommandType.Copy, () => Copy()); Command.register(
CommandType.Copy,
() => Copy(),
spec: const CommandSpec(
type: CommandType.Copy,
category: 'file',
dataModel: 'DataCopy',
samplePath: 'assets/yaml/sample/07_file.yaml',
),
);
} }

View file

@ -61,5 +61,14 @@ class Delete extends Command {
} }
void registerDeleteCommand() { void registerDeleteCommand() {
Command.register(CommandType.Delete, () => Delete()); Command.register(
CommandType.Delete,
() => Delete(),
spec: const CommandSpec(
type: CommandType.Delete,
category: 'file',
dataModel: 'DataDelete',
samplePath: 'assets/yaml/sample/07_file.yaml',
),
);
} }

View file

@ -19,5 +19,13 @@ class DirectoryCreate extends Command {
} }
} }
void registerDirectoryCommand() { void registerDirectoryCommand() {
Command.register(CommandType.DirectoryCreate, () => DirectoryCreate()); Command.register(
CommandType.DirectoryCreate,
() => DirectoryCreate(),
spec: const CommandSpec(
type: CommandType.DirectoryCreate,
category: 'file',
dataModel: 'DataDirectoryCreate',
),
);
} }

View file

@ -108,8 +108,43 @@ class FileInfo extends Command {
} }
void registerFileCommands() { void registerFileCommands() {
Command.register(CommandType.FileRead, () => FileRead()); Command.register(
Command.register(CommandType.FileWrite, () => FileWrite()); CommandType.FileRead,
Command.register(CommandType.Files, () => Files()); () => FileRead(),
Command.register(CommandType.FileInfo, () => FileInfo()); spec: const CommandSpec(
type: CommandType.FileRead,
category: 'file',
dataModel: 'DataFileRead',
samplePath: 'assets/yaml/sample/07_file.yaml',
),
);
Command.register(
CommandType.FileWrite,
() => FileWrite(),
spec: const CommandSpec(
type: CommandType.FileWrite,
category: 'file',
dataModel: 'DataFileWrite',
samplePath: 'assets/yaml/sample/07_file.yaml',
),
);
Command.register(
CommandType.Files,
() => Files(),
spec: const CommandSpec(
type: CommandType.Files,
category: 'file',
dataModel: 'DataFiles',
),
);
Command.register(
CommandType.FileInfo,
() => FileInfo(),
spec: const CommandSpec(
type: CommandType.FileInfo,
category: 'file',
dataModel: 'DataFileInfo',
samplePath: 'assets/yaml/sample/07_file.yaml',
),
);
} }

View file

@ -125,5 +125,13 @@ class FileDiffCheck extends Command {
} }
void registerFileDiffCheckCommand() { void registerFileDiffCheckCommand() {
Command.register(CommandType.FileDiffCheck, () => FileDiffCheck()); Command.register(
CommandType.FileDiffCheck,
() => FileDiffCheck(),
spec: const CommandSpec(
type: CommandType.FileDiffCheck,
category: 'file',
dataModel: 'DataFileDiffCheck',
),
);
} }

View file

@ -37,5 +37,13 @@ class Rename extends Command {
} }
void registerRenameCommand() { void registerRenameCommand() {
Command.register(CommandType.Rename, () => Rename()); Command.register(
CommandType.Rename,
() => Rename(),
spec: const CommandSpec(
type: CommandType.Rename,
category: 'file',
dataModel: 'DataRename',
),
);
} }

View file

@ -78,5 +78,14 @@ class Zip extends Command {
} }
void registerZipCommand() { void registerZipCommand() {
Command.register(CommandType.Zip, () => Zip()); Command.register(
CommandType.Zip,
() => Zip(),
spec: const CommandSpec(
type: CommandType.Zip,
category: 'file',
dataModel: 'DataZip',
samplePath: 'assets/yaml/sample/07_file.yaml',
),
);
} }

View file

@ -45,5 +45,14 @@ class Download extends Command {
} }
void registerFtpDownloadCommand() { void registerFtpDownloadCommand() {
Command.register(CommandType.Download, () => Download()); Command.register(
CommandType.Download,
() => Download(),
spec: const CommandSpec(
type: CommandType.Download,
category: 'ftp',
dataModel: 'DataFTP',
samplePath: 'assets/yaml/sample/09_network.yaml',
),
);
} }

View file

@ -74,5 +74,14 @@ class Upload extends Command {
} }
void registerFtpUploadCommand() { void registerFtpUploadCommand() {
Command.register(CommandType.Upload, () => Upload()); Command.register(
CommandType.Upload,
() => Upload(),
spec: const CommandSpec(
type: CommandType.Upload,
category: 'ftp',
dataModel: 'DataFTP',
samplePath: 'assets/yaml/sample/09_network.yaml',
),
);
} }

View file

@ -1,11 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dart_framework/platform/process.dart';
import 'package:dart_framework/utils/system_util.dart'; import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/oto/application.dart'; import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command.dart'; import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/data/command_data.dart'; import 'package:oto/oto/data/command_data.dart';
import 'package:dart_framework/platform/process.dart';
class Git extends Command { class Git extends Command {
@override @override
@ -20,7 +20,7 @@ class Git extends Command {
} }
var process = var process =
await ProcessExecutor.start(shell, logHandler: Application.logWithType); await runtime.start(shell);
return await completeProcess(process, command); return await completeProcess(process, command);
} }
} }
@ -42,7 +42,7 @@ class GitCommit extends Command {
} }
var process = var process =
await ProcessExecutor.start(shell, logHandler: Application.logWithType); await runtime.start(shell);
return await completeProcess(process, command); return await completeProcess(process, command);
} }
} }
@ -58,7 +58,7 @@ class GitPull extends Command {
shell.write(' $and git pull origin ${data.branch}'); shell.write(' $and git pull origin ${data.branch}');
var process = var process =
await ProcessExecutor.start(shell, logHandler: Application.logWithType); await runtime.start(shell);
return await completeProcess(process, command, return await completeProcess(process, command,
passExitCodes: [1 /*Everything up-to-date*/]); passExitCodes: [1 /*Everything up-to-date*/]);
} }
@ -80,7 +80,7 @@ class GitPush extends Command {
} }
var process = var process =
await ProcessExecutor.start(shell, logHandler: Application.logWithType); await runtime.start(shell);
return await completeProcess(process, command, return await completeProcess(process, command,
passExitCodes: [1 /*Everything up-to-date*/]); passExitCodes: [1 /*Everything up-to-date*/]);
} }
@ -98,7 +98,7 @@ class GitCheckout extends Git {
shell.write(' $and git checkout -b $branch'); shell.write(' $and git checkout -b $branch');
var process = var process =
await ProcessExecutor.start(shell, logHandler: Application.logWithType); await runtime.start(shell);
var exitCode = await process.process.exitCode; var exitCode = await process.process.exitCode;
var ignores = [128 /*Already exists*/]; var ignores = [128 /*Already exists*/];
if (ignores.contains(exitCode)) { if (ignores.contains(exitCode)) {
@ -113,8 +113,7 @@ class GitCheckout extends Git {
shell.write(' $and git pull origin $branch'); shell.write(' $and git pull origin $branch');
} }
process = await ProcessExecutor.start(shell, process = await runtime.start(shell);
logHandler: Application.logWithType);
exitCode = await process.process.exitCode; exitCode = await process.process.exitCode;
} }
@ -130,8 +129,7 @@ class GitCount extends Command {
var shell = StringBuffer(getCDPath(workspace)); var shell = StringBuffer(getCDPath(workspace));
shell.write(' $and git rev-list --all HEAD --all --count'); shell.write(' $and git rev-list --all HEAD --all --count');
var process = await ProcessExecutor.start(shell, var process = await runtime.start(shell, printStderr: false);
printStderr: false, logHandler: Application.logWithType);
var number = int.parse(process.stdoutArr.last); var number = int.parse(process.stdoutArr.last);
await setProperty(data.setValue, number); await setProperty(data.setValue, number);
@ -149,8 +147,7 @@ class GitRev extends Command {
var target = data.branch ?? 'HEAD'; var target = data.branch ?? 'HEAD';
shell.write(' $and git fetch origin'); shell.write(' $and git fetch origin');
shell.write(' $and git rev-parse $target'); shell.write(' $and git rev-parse $target');
var process = await ProcessExecutor.start(shell, var process = await runtime.start(shell, printStderr: false);
printStderr: false, logHandler: Application.logWithType);
var hash = process.stdoutArr.last; var hash = process.stdoutArr.last;
await setProperty(data.setValue, hash); await setProperty(data.setValue, hash);
@ -168,8 +165,7 @@ class GitReset extends Command {
var force = data.force! ? 'f' : ''; var force = data.force! ? 'f' : '';
shell.write(' $and git reset --hard'); shell.write(' $and git reset --hard');
shell.write(' $and git clean -${force}d'); shell.write(' $and git clean -${force}d');
var process = await ProcessExecutor.start(shell, var process = await runtime.start(shell, printStderr: false);
printStderr: false, logHandler: Application.logWithType);
return await completeProcess(process, command); return await completeProcess(process, command);
} }
@ -187,7 +183,7 @@ class GitStashPush extends Command {
: (data.includeUntracked! ? " -u" : ""); : (data.includeUntracked! ? " -u" : "");
var message = data.message == null ? "" : " -m '${data.message}'"; var message = data.message == null ? "" : " -m '${data.message}'";
shell.write(' $and git stash push$includeUntracked$message'); shell.write(' $and git stash push$includeUntracked$message');
var process = await ProcessExecutor.run(shell); var process = await runtime.run(shell);
if (data.setSuccess != null) { if (data.setSuccess != null) {
setProperty( setProperty(
data.setSuccess!, process.stdout.toString().contains('Saved')); data.setSuccess!, process.stdout.toString().contains('Saved'));
@ -208,11 +204,10 @@ class GitStashApply extends Command {
data.delete == null ? " pop" : (data.delete! ? " pop" : " apply"); data.delete == null ? " pop" : (data.delete! ? " pop" : " apply");
if (data.message == null) { if (data.message == null) {
shell.write(' $and git stash$apply'); shell.write(' $and git stash$apply');
process = await ProcessExecutor.start(shell, process = await runtime.start(shell, printStderr: false);
printStderr: false, logHandler: Application.logWithType);
} else { } else {
shell.write(' $and git stash list'); shell.write(' $and git stash list');
var result = await ProcessExecutor.run(shell); var result = await runtime.run(shell);
var exist = false; var exist = false;
Application.log(result.stdout); Application.log(result.stdout);
shell = StringBuffer(getCDPath(workspace)); shell = StringBuffer(getCDPath(workspace));
@ -233,8 +228,7 @@ class GitStashApply extends Command {
} }
if (exist) { if (exist) {
process = await ProcessExecutor.start(shell, process = await runtime.start(shell, printStderr: false);
printStderr: false, logHandler: Application.logWithType);
} else { } else {
return await complete(0, command); return await complete(0, command);
} }
@ -254,7 +248,7 @@ class GitBranch extends Command {
' $and git for-each-ref --format=\'{"ref": "%(refname:short)", "sha": "%(objectname)", "date": "%(committerdate:iso8601)"}\' refs/remotes | jq -s . > branches.json'); ' $and git for-each-ref --format=\'{"ref": "%(refname:short)", "sha": "%(objectname)", "date": "%(committerdate:iso8601)"}\' refs/remotes | jq -s . > branches.json');
var process = var process =
await ProcessExecutor.start(shell, logHandler: Application.logWithType); await runtime.start(shell);
var file = File('${data.path}/branches.json'); var file = File('${data.path}/branches.json');
var content = file.readAsStringSync(); var content = file.readAsStringSync();
@ -294,15 +288,110 @@ class GitBranch extends Command {
} }
void registerGitCommands() { void registerGitCommands() {
Command.register(CommandType.Git, () => Git()); Command.register(
Command.register(CommandType.GitCommit, () => GitCommit()); CommandType.Git,
Command.register(CommandType.GitPull, () => GitPull()); () => Git(),
Command.register(CommandType.GitPush, () => GitPush()); spec: const CommandSpec(
Command.register(CommandType.GitCheckout, () => GitCheckout()); type: CommandType.Git,
Command.register(CommandType.GitRev, () => GitRev()); category: 'git',
Command.register(CommandType.GitCount, () => GitCount()); dataModel: 'DataGit',
Command.register(CommandType.GitReset, () => GitReset()); samplePath: 'assets/yaml/sample/06_git.yaml',
Command.register(CommandType.GitStashPush, () => GitStashPush()); ),
Command.register(CommandType.GitStashApply, () => GitStashApply()); );
Command.register(CommandType.GitBranch, () => GitBranch()); Command.register(
CommandType.GitCommit,
() => GitCommit(),
spec: const CommandSpec(
type: CommandType.GitCommit,
category: 'git',
dataModel: 'DataGitCommit',
samplePath: 'assets/yaml/sample/06_git.yaml',
),
);
Command.register(
CommandType.GitPull,
() => GitPull(),
spec: const CommandSpec(
type: CommandType.GitPull,
category: 'git',
dataModel: 'DataGitPull',
),
);
Command.register(
CommandType.GitPush,
() => GitPush(),
spec: const CommandSpec(
type: CommandType.GitPush,
category: 'git',
dataModel: 'DataGitPush',
samplePath: 'assets/yaml/sample/06_git.yaml',
),
);
Command.register(
CommandType.GitCheckout,
() => GitCheckout(),
spec: const CommandSpec(
type: CommandType.GitCheckout,
category: 'git',
dataModel: 'DataGitCheckout',
samplePath: 'assets/yaml/sample/06_git.yaml',
),
);
Command.register(
CommandType.GitRev,
() => GitRev(),
spec: const CommandSpec(
type: CommandType.GitRev,
category: 'git',
dataModel: 'DataGitRev',
samplePath: 'assets/yaml/sample/06_git.yaml',
),
);
Command.register(
CommandType.GitCount,
() => GitCount(),
spec: const CommandSpec(
type: CommandType.GitCount,
category: 'git',
dataModel: 'DataGitCount',
samplePath: 'assets/yaml/sample/06_git.yaml',
),
);
Command.register(
CommandType.GitReset,
() => GitReset(),
spec: const CommandSpec(
type: CommandType.GitReset,
category: 'git',
dataModel: 'DataGitReset',
),
);
Command.register(
CommandType.GitStashPush,
() => GitStashPush(),
spec: const CommandSpec(
type: CommandType.GitStashPush,
category: 'git',
dataModel: 'DataGitStashPush',
),
);
Command.register(
CommandType.GitStashApply,
() => GitStashApply(),
spec: const CommandSpec(
type: CommandType.GitStashApply,
category: 'git',
dataModel: 'DataGitStashApply',
),
);
Command.register(
CommandType.GitBranch,
() => GitBranch(),
spec: const CommandSpec(
type: CommandType.GitBranch,
category: 'git',
dataModel: 'DataGitBranch',
samplePath: 'assets/yaml/sample/06_git.yaml',
),
);
} }

View file

@ -124,8 +124,40 @@ class GitHubPullRequestClose extends Command {
} }
void registerGitHubCommands() { void registerGitHubCommands() {
Command.register(CommandType.GitHub, () => GitHub()); Command.register(
Command.register(CommandType.GitHubPullRequestCreate, () => GitHubPullRequestCreate()); CommandType.GitHub,
Command.register(CommandType.GitHubPullRequestList, () => GitHubPullRequestList()); () => GitHub(),
Command.register(CommandType.GitHubPullRequestClose, () => GitHubPullRequestClose()); spec: const CommandSpec(
type: CommandType.GitHub,
category: 'github',
dataModel: 'DataGitHub',
),
);
Command.register(
CommandType.GitHubPullRequestCreate,
() => GitHubPullRequestCreate(),
spec: const CommandSpec(
type: CommandType.GitHubPullRequestCreate,
category: 'github',
dataModel: 'DataGitHubPullRequestCreate',
),
);
Command.register(
CommandType.GitHubPullRequestList,
() => GitHubPullRequestList(),
spec: const CommandSpec(
type: CommandType.GitHubPullRequestList,
category: 'github',
dataModel: 'DataGitHubPullRequestList',
),
);
Command.register(
CommandType.GitHubPullRequestClose,
() => GitHubPullRequestClose(),
spec: const CommandSpec(
type: CommandType.GitHubPullRequestClose,
category: 'github',
dataModel: 'DataGitHubPullRequestClose',
),
);
} }

View file

@ -24,5 +24,13 @@ class Gradle extends Command {
} }
void registerGradleCommand() { void registerGradleCommand() {
Command.register(CommandType.Gradle, () => Gradle()); Command.register(
CommandType.Gradle,
() => Gradle(),
spec: const CommandSpec(
type: CommandType.Gradle,
category: 'gradle',
dataModel: 'DataGradle',
),
);
} }

View file

@ -25,5 +25,13 @@ class SMBAuth extends Command {
} }
void registerInfraCommands() { void registerInfraCommands() {
Command.register(CommandType.SMBAuth, () => SMBAuth()); Command.register(
CommandType.SMBAuth,
() => SMBAuth(),
spec: const CommandSpec(
type: CommandType.SMBAuth,
category: 'infra',
dataModel: 'DataSMBAuthorize',
),
);
} }

View file

@ -65,5 +65,13 @@ class JenkinsParameterModify extends Command {
} }
void registerJenkinsCommands() { void registerJenkinsCommands() {
Command.register(CommandType.JenkinsParameterModify, () => JenkinsParameterModify()); Command.register(
CommandType.JenkinsParameterModify,
() => JenkinsParameterModify(),
spec: const CommandSpec(
type: CommandType.JenkinsParameterModify,
category: 'jenkins',
dataModel: 'DataJenkinsParameterModify',
),
);
} }

View file

@ -123,5 +123,13 @@ class Jira extends Command {
} }
} }
void registerJiraCommand() { void registerJiraCommand() {
Command.register(CommandType.Jira, () => Jira()); Command.register(
CommandType.Jira,
() => Jira(),
spec: const CommandSpec(
type: CommandType.Jira,
category: 'jira',
dataModel: 'DataJira',
),
);
} }

View file

@ -97,6 +97,24 @@ class MattermostBuild extends Command {
} }
void registerMattermostCommands() { void registerMattermostCommands() {
Command.register(CommandType.Mattermost, () => Mattermost()); Command.register(
Command.register(CommandType.MattermostBuild, () => MattermostBuild()); CommandType.Mattermost,
() => Mattermost(),
spec: const CommandSpec(
type: CommandType.Mattermost,
category: 'notification',
dataModel: 'DataMattermostSender',
samplePath: 'assets/yaml/sample/08_notification.yaml',
),
);
Command.register(
CommandType.MattermostBuild,
() => MattermostBuild(),
spec: const CommandSpec(
type: CommandType.MattermostBuild,
category: 'notification',
dataModel: 'DataMattermostBuild',
samplePath: 'assets/yaml/sample/08_notification.yaml',
),
);
} }

View file

@ -79,7 +79,33 @@ class SlackBuild extends Command {
} }
void registerSlackCommands() { void registerSlackCommands() {
Command.register(CommandType.Slack, () => Slack()); Command.register(
Command.register(CommandType.SlackFile, () => SlackFile()); CommandType.Slack,
Command.register(CommandType.SlackBuild, () => SlackBuild()); () => Slack(),
spec: const CommandSpec(
type: CommandType.Slack,
category: 'notification',
dataModel: 'DataSlackSender',
samplePath: 'assets/yaml/sample/08_notification.yaml',
),
);
Command.register(
CommandType.SlackFile,
() => SlackFile(),
spec: const CommandSpec(
type: CommandType.SlackFile,
category: 'notification',
dataModel: 'DataSlackFile',
),
);
Command.register(
CommandType.SlackBuild,
() => SlackBuild(),
spec: const CommandSpec(
type: CommandType.SlackBuild,
category: 'notification',
dataModel: 'DataSlackBuild',
samplePath: 'assets/yaml/sample/08_notification.yaml',
),
);
} }

View file

@ -34,13 +34,7 @@ class ProcessRun extends Command {
Platform.isWindows ? 'SET BUILD_ID=dontKillMe' : 'BUILD_ID=dontKillMe'; Platform.isWindows ? 'SET BUILD_ID=dontKillMe' : 'BUILD_ID=dontKillMe';
var param = data.param == null ? '' : ' ${data.param}'; var param = data.param == null ? '' : ' ${data.param}';
shell.writeln(' $and $dontKillme $and $exe$param'); shell.writeln(' $and $dontKillme $and $exe$param');
var file = await runtime.startDetached(shell, workspace: data.workspace!);
File('${Directory.systemTemp.path}/${ProcessExecutor.tempFileName}');
await file.writeAsString(shell.toString());
await Process.start(file.path, [],
runInShell: true,
workingDirectory: data.workspace,
mode: ProcessStartMode.detached);
return complete(0, command); return complete(0, command);
} }
} }
@ -59,7 +53,34 @@ class ProcessKill extends Command {
} }
void registerProcessCommands() { void registerProcessCommands() {
Command.register(CommandType.ProcessPortUse, () => ProcessPortUse()); Command.register(
Command.register(CommandType.ProcessRun, () => ProcessRun()); CommandType.ProcessPortUse,
Command.register(CommandType.ProcessKill, () => ProcessKill()); () => ProcessPortUse(),
spec: const CommandSpec(
type: CommandType.ProcessPortUse,
category: 'process',
dataModel: 'DataProcessPortUse',
samplePath: 'assets/yaml/sample/14_shell_process.yaml',
),
);
Command.register(
CommandType.ProcessRun,
() => ProcessRun(),
spec: const CommandSpec(
type: CommandType.ProcessRun,
category: 'process',
dataModel: 'DataProcessRun',
samplePath: 'assets/yaml/sample/14_shell_process.yaml',
),
);
Command.register(
CommandType.ProcessKill,
() => ProcessKill(),
spec: const CommandSpec(
type: CommandType.ProcessKill,
category: 'process',
dataModel: 'DataProcessKill',
samplePath: 'assets/yaml/sample/14_shell_process.yaml',
),
);
} }

View file

@ -23,5 +23,13 @@ class Protobuf extends Command {
} }
void registerProtobufCommand() { void registerProtobufCommand() {
Command.register(CommandType.Protobuf, () => Protobuf()); Command.register(
CommandType.Protobuf,
() => Protobuf(),
spec: const CommandSpec(
type: CommandType.Protobuf,
category: 'proto',
dataModel: 'DataProtobuf',
),
);
} }

View file

@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:dart_framework/platform/process.dart';
import 'package:dart_framework/utils/system_util.dart'; import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command.dart'; import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/data/command_data.dart'; import 'package:oto/oto/data/command_data.dart';
@ -16,8 +14,7 @@ class Shell extends Command {
shell.write(' $and $item'); shell.write(' $and $item');
} }
var process = var process = await runtime.start(shell);
await ProcessExecutor.start(shell, logHandler: Application.logWithType);
if (data.setMessage == null) { if (data.setMessage == null) {
return await completeProcess(process, command); return await completeProcess(process, command);
} else { } else {
@ -42,8 +39,7 @@ class ShellFile extends Command {
} else { } else {
shell.writeln('source $path'); shell.writeln('source $path');
} }
var process = var process = await runtime.start(shell);
await ProcessExecutor.start(shell, logHandler: Application.logWithType);
return await completeProcess(process, command); return await completeProcess(process, command);
} }
@ -58,6 +54,24 @@ class ShellFile extends Command {
} }
void registerShellCommands() { void registerShellCommands() {
Command.register(CommandType.Shell, () => Shell()); Command.register(
Command.register(CommandType.ShellFile, () => ShellFile()); CommandType.Shell,
() => Shell(),
spec: const CommandSpec(
type: CommandType.Shell,
category: 'shell',
dataModel: 'DataShell',
samplePath: 'assets/yaml/sample/14_shell_process.yaml',
),
);
Command.register(
CommandType.ShellFile,
() => ShellFile(),
spec: const CommandSpec(
type: CommandType.ShellFile,
category: 'shell',
dataModel: 'DataShellFile',
samplePath: 'assets/yaml/sample/14_shell_process.yaml',
),
);
} }

View file

@ -136,7 +136,34 @@ class JsonWriterFile extends Command {
} }
void registerJsonCommands() { void registerJsonCommands() {
Command.register(CommandType.JsonReader, () => JsonReader()); Command.register(
Command.register(CommandType.JsonReaderFile, () => JsonReaderFile()); CommandType.JsonReader,
Command.register(CommandType.JsonWriterFile, () => JsonWriterFile()); () => JsonReader(),
spec: const CommandSpec(
type: CommandType.JsonReader,
category: 'util',
dataModel: 'DataJsonReader',
samplePath: 'assets/yaml/sample/13_string_json.yaml',
),
);
Command.register(
CommandType.JsonReaderFile,
() => JsonReaderFile(),
spec: const CommandSpec(
type: CommandType.JsonReaderFile,
category: 'util',
dataModel: 'DataJsonReaderFile',
samplePath: 'assets/yaml/sample/13_string_json.yaml',
),
);
Command.register(
CommandType.JsonWriterFile,
() => JsonWriterFile(),
spec: const CommandSpec(
type: CommandType.JsonWriterFile,
category: 'util',
dataModel: 'DataJsonWriterFile',
samplePath: 'assets/yaml/sample/13_string_json.yaml',
),
);
} }

View file

@ -17,5 +17,14 @@ class PrintCommand extends Command {
} }
void registerPrintCommand() { void registerPrintCommand() {
Command.register(CommandType.Print, () => PrintCommand()); Command.register(
CommandType.Print,
() => PrintCommand(),
spec: const CommandSpec(
type: CommandType.Print,
category: 'util',
dataModel: 'DataPrint',
samplePath: 'assets/yaml/sample/01_basic.yaml',
),
);
} }

View file

@ -87,5 +87,14 @@ class SetValueCommand extends Command {
} }
void registerSetValueCommand() { void registerSetValueCommand() {
Command.register(CommandType.SetValue, () => SetValueCommand()); Command.register(
CommandType.SetValue,
() => SetValueCommand(),
spec: const CommandSpec(
type: CommandType.SetValue,
category: 'util',
dataModel: 'DataSetValue',
samplePath: 'assets/yaml/sample/01_basic.yaml',
),
);
} }

View file

@ -115,9 +115,53 @@ class URLInfo extends Command {
void registerStringUtilCommands() { void registerStringUtilCommands() {
Command.register(CommandType.StringSub, () => StringSub()); Command.register(
Command.register(CommandType.StringReplace, () => StringReplace()); CommandType.StringSub,
Command.register(CommandType.StringReplacePattern, () => StringReplacePattern()); () => StringSub(),
Command.register(CommandType.StringIndex, () => StringIndex()); spec: const CommandSpec(
Command.register(CommandType.URLInfo, () => URLInfo()); type: CommandType.StringSub,
category: 'util',
dataModel: 'DataStringSub',
samplePath: 'assets/yaml/sample/13_string_json.yaml',
),
);
Command.register(
CommandType.StringReplace,
() => StringReplace(),
spec: const CommandSpec(
type: CommandType.StringReplace,
category: 'util',
dataModel: 'DataStringReplace',
samplePath: 'assets/yaml/sample/13_string_json.yaml',
),
);
Command.register(
CommandType.StringReplacePattern,
() => StringReplacePattern(),
spec: const CommandSpec(
type: CommandType.StringReplacePattern,
category: 'util',
dataModel: 'DataStringReplacePattern',
samplePath: 'assets/yaml/sample/13_string_json.yaml',
),
);
Command.register(
CommandType.StringIndex,
() => StringIndex(),
spec: const CommandSpec(
type: CommandType.StringIndex,
category: 'util',
dataModel: 'DataStringIndex',
samplePath: 'assets/yaml/sample/13_string_json.yaml',
),
);
Command.register(
CommandType.URLInfo,
() => URLInfo(),
spec: const CommandSpec(
type: CommandType.URLInfo,
category: 'util',
dataModel: 'DataURLInfo',
),
);
} }

View file

@ -34,5 +34,13 @@ class Delay extends Command {
} }
} }
void registerTimerCommand() { void registerTimerCommand() {
Command.register(CommandType.Delay, () => Delay()); Command.register(
CommandType.Delay,
() => Delay(),
spec: const CommandSpec(
type: CommandType.Delay,
category: 'util',
dataModel: 'DataDelay',
),
);
} }

View file

@ -82,6 +82,24 @@ class WebFile extends WebBase {
} }
void registerWebCommands() { void registerWebCommands() {
Command.register(CommandType.WebRequest, () => WebRequest()); Command.register(
Command.register(CommandType.WebFile, () => WebFile()); CommandType.WebRequest,
() => WebRequest(),
spec: const CommandSpec(
type: CommandType.WebRequest,
category: 'web',
dataModel: 'DataWebBase',
samplePath: 'assets/yaml/sample/09_network.yaml',
),
);
Command.register(
CommandType.WebFile,
() => WebFile(),
spec: const CommandSpec(
type: CommandType.WebFile,
category: 'web',
dataModel: 'DataWebFile',
samplePath: 'assets/yaml/sample/09_network.yaml',
),
);
} }

View file

@ -0,0 +1,9 @@
import 'package:oto/oto/application.dart';
import 'package:oto/oto/data/command_data.dart';
class ExecutionContext {
DataCommon? commonData;
Map<String, dynamic> property = {};
Map<String, CommandState> commandStates = {};
Map<String, DataCommand> dataCommandMap = {};
}

View file

@ -106,7 +106,7 @@ class TagSystem {
tag = match.group(2)!; tag = match.group(2)!;
if (tag.startsWith('property')) { if (tag.startsWith('property')) {
var key = tag.replaceFirst('property.', ''); var key = tag.replaceFirst('property.', '');
Application.instance.property[key] = value; Application.instance.context.property[key] = value;
if (showPrint ?? true) { if (showPrint ?? true) {
if (value is List) { if (value is List) {
Application.log('[Return] $key :\n'); Application.log('[Return] $key :\n');
@ -127,14 +127,14 @@ class TagSystem {
/// Looks up the actual value for a tag name. /// Looks up the actual value for a tag name.
/// ///
/// - `property.key` `Application.instance.property[key]` /// - `property.key` `Application.instance.context.property[key]`
/// - `property.nested.key` traverses nested Maps /// - `property.nested.key` traverses nested Maps
/// - `state.commandId` `Application.instance.commandStates[commandId]` /// - `state.commandId` `Application.instance.context.commandStates[commandId]`
static dynamic _getTagValue(String value) { static dynamic _getTagValue(String value) {
var setted = false; var setted = false;
dynamic data; dynamic data;
if (value.startsWith('property.')) { if (value.startsWith('property.')) {
var map = Application.instance.property; var map = Application.instance.context.property;
var param = value.replaceFirst('property.', ''); var param = value.replaceFirst('property.', '');
if (map.containsKey(param)) { if (map.containsKey(param)) {
data = map[param]; data = map[param];
@ -165,7 +165,7 @@ class TagSystem {
} }
if (value.startsWith('state.')) { if (value.startsWith('state.')) {
var map = Application.instance.commandStates; var map = Application.instance.context.commandStates;
var param = value.replaceFirst('state.', ''); var param = value.replaceFirst('state.', '');
if (map.containsKey(param)) { if (map.containsKey(param)) {
var value = map[param].toString().replaceAll('CommandState.', ''); var value = map[param].toString().replaceAll('CommandState.', '');

View file

@ -0,0 +1,97 @@
import 'dart:io';
import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_registry.dart';
import 'package:test/test.dart';
void main() {
setUpAll(() {
registerAllCommands();
});
test('all command types are registered', () {
final registered = Command.registeredTypes.toSet();
final missing =
CommandType.values.where((t) => !registered.contains(t)).toList();
expect(
missing,
isEmpty,
reason:
'CommandType values without a Command.register() call: $missing. '
'Add registration in the relevant register*() function and call it from registerAllCommands().',
);
});
test('all registered commands expose specs', () {
final specs = Command.specs;
final missing = Command.registeredTypes
.where((t) => !specs.containsKey(t))
.toList();
expect(
missing,
isEmpty,
reason:
'Registered commands without a CommandSpec: $missing. '
'Pass spec: CommandSpec(...) to Command.register().',
);
});
test('spec sample paths exist when provided', () {
final missing = <String>[];
for (final entry in Command.specs.entries) {
final sample = entry.value.samplePath;
if (sample == null) continue;
if (!File(sample).existsSync()) {
missing.add('${entry.key} -> $sample');
}
}
expect(
missing,
isEmpty,
reason:
'CommandSpec.samplePath must point to an existing file. '
'Missing: $missing',
);
});
test('spec sample paths contain the registered command', () {
final mismatches = <String>[];
for (final entry in Command.specs.entries) {
final sample = entry.value.samplePath;
if (sample == null) continue;
final file = File(sample);
if (!file.existsSync()) continue;
final commandName = entry.key.name;
final pattern =
RegExp(r'^\s*-\s*command:\s*' + RegExp.escape(commandName) + r'\s*$');
final hasCommand = file.readAsLinesSync().any((line) {
final trimmed = line.trimLeft();
if (trimmed.startsWith('#')) return false;
return pattern.hasMatch(line);
});
if (!hasCommand) {
mismatches.add('${entry.key} -> $sample');
}
}
expect(
mismatches,
isEmpty,
reason:
'CommandSpec.samplePath must reference a sample that actually '
'includes a non-commented `- command: <Type>` line. Mismatches: '
'$mismatches',
);
});
test('spec category and dataModel are non-empty', () {
final bad = <String>[];
for (final entry in Command.specs.entries) {
final spec = entry.value;
if (spec.category.isEmpty || spec.dataModel.isEmpty) {
bad.add('${entry.key}');
}
}
expect(bad, isEmpty,
reason: 'CommandSpec.category and dataModel must be non-empty: $bad');
});
}

View file

@ -0,0 +1,138 @@
// ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api
import 'dart:async';
import 'dart:io';
import 'package:dart_framework/platform/process.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command_runtime.dart';
import 'package:oto/oto/commands/git/git.dart';
import 'package:oto/oto/commands/process/process.dart';
import 'package:oto/oto/commands/shell/shell.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:test/test.dart';
class _StartCall {
final String shell;
final bool printStderr;
_StartCall(this.shell, this.printStderr);
}
class _DetachedCall {
final String shell;
final String workspace;
_DetachedCall(this.shell, this.workspace);
}
class FakeProcessData extends ProcessData {
FakeProcessData(
{int exitCode = 0, String stdoutText = '', String stderrText = ''})
: super(false, false) {
this.exitCode = exitCode;
this.stdout = StringBuffer(stdoutText);
this.stderr = StringBuffer(stderrText);
}
@override
Future waitForExit() async {}
@override
Process get process =>
throw UnsupportedError('FakeProcessData has no real process');
}
class FakeRuntime implements CommandRuntime {
final List<_StartCall> startCalls = [];
final List<String> runCalls = [];
final List<_DetachedCall> detachedCalls = [];
int exitCode;
String stdoutText;
FakeRuntime({this.exitCode = 0, this.stdoutText = ''});
@override
Future<ProcessData> start(StringBuffer shell,
{bool printStderr = true, LogHandler? logHandler}) async {
startCalls.add(_StartCall(shell.toString(), printStderr));
return FakeProcessData(exitCode: exitCode, stdoutText: stdoutText);
}
@override
Future<ProcessResult> run(StringBuffer shell) async {
runCalls.add(shell.toString());
return ProcessResult(0, exitCode, stdoutText, '');
}
@override
Future<void> startDetached(StringBuffer shell,
{required String workspace}) async {
detachedCalls.add(_DetachedCall(shell.toString(), workspace));
}
}
void main() {
setUp(() {
Application.instance.context = ExecutionContext();
Application.instance.property['workspace'] = '/tmp/oto_runtime_test';
});
DataCommand commandWith(Map<String, dynamic> param) {
return DataCommand.fromJson({
'command': 'Shell',
'id': 'fake',
'param': param,
});
}
test('shell command delegates process start to runtime', () async {
final fake = FakeRuntime();
final shell = Shell()..runtime = fake;
final command = commandWith({
'commands': ['echo hello'],
});
await shell.execute(command);
expect(fake.startCalls, hasLength(1));
expect(fake.startCalls.single.shell, contains('echo hello'));
expect(fake.startCalls.single.shell, contains('/tmp/oto_runtime_test'));
expect(fake.runCalls, isEmpty);
expect(fake.detachedCalls, isEmpty);
});
test('git command delegates command buffer to runtime', () async {
final fake = FakeRuntime();
final git = Git()..runtime = fake;
final command = commandWith({
'commands': ['status', 'fetch origin'],
});
await git.execute(command);
expect(fake.startCalls, hasLength(1));
final buffer = fake.startCalls.single.shell;
expect(buffer, contains('git status'));
expect(buffer, contains('git fetch origin'));
expect(fake.detachedCalls, isEmpty);
});
test('process run delegates detached start to runtime', () async {
final fake = FakeRuntime();
final processRun = ProcessRun()..runtime = fake;
final command = commandWith({
'executable': 'my_server',
'param': '--port 8080',
'workspace': '/tmp/oto_runtime_test',
});
await processRun.execute(command);
expect(fake.detachedCalls, hasLength(1));
expect(fake.detachedCalls.single.workspace, '/tmp/oto_runtime_test');
expect(fake.detachedCalls.single.shell, contains('my_server'));
expect(fake.detachedCalls.single.shell, contains('--port 8080'));
expect(fake.detachedCalls.single.shell, contains('dontKillMe'));
expect(fake.startCalls, isEmpty);
});
}

View file

@ -0,0 +1,33 @@
import 'package:oto/oto/application.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/core/tag_system.dart';
import 'package:test/test.dart';
void main() {
setUp(() {
Application.instance.context = ExecutionContext();
});
test('context backs application compatibility accessors', () {
Application.instance.property['workspace'] = '/tmp/oto';
Application.instance.commandStates['step'] = CommandState.progress;
expect(Application.instance.context.property['workspace'], '/tmp/oto');
expect(
Application.instance.context.commandStates['step'], CommandState.progress);
Application.instance.context.property['key'] = 'value';
expect(Application.instance.property['key'], 'value');
});
test('tag system reads and writes through execution context', () async {
await TagSystem.setPropertyValue('<@property.appVersion>', '1.2.3',
showPrint: false);
expect(Application.instance.context.property['appVersion'], '1.2.3');
expect(TagSystem.replaceTagValue('<!property.appVersion>'), '1.2.3');
Application.instance.context.commandStates['build'] = CommandState.complete;
expect(TagSystem.replaceTagValue('<!state.build>'), 'complete');
});
}