chore: update configs/edge.yaml and add agent-task directories
This commit is contained in:
parent
224b649d23
commit
eb8834ac5c
15 changed files with 1570 additions and 1 deletions
58
agent-task/01_cli_capabilities_real_profiles/CODE_REVIEW.md
Normal file
58
agent-task/01_cli_capabilities_real_profiles/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<!-- task=01_cli_capabilities_real_profiles plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=01_cli_capabilities_real_profiles, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log`
|
||||
2. `PLAN.md` → `plan_M.log`
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] knownAgents 제거 및 Capabilities 단순화 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `knownAgents` 심볼이 코드베이스 어디에도 남아있지 않은지 (`rg knownAgents`)
|
||||
- `Capabilities().Models`가 정렬된 상태로 반환되어 결과가 결정적인지
|
||||
- 빈 profiles에 대해 `Models`가 nil이 아닌 길이 0 슬라이스인지
|
||||
- 다른 어댑터(mock/ollama/vllm)의 Capabilities 동작에 영향 없는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./apps/node/...
|
||||
$ rg -n 'knownAgents' apps/node/
|
||||
(output)
|
||||
```
|
||||
84
agent-task/01_cli_capabilities_real_profiles/PLAN.md
Normal file
84
agent-task/01_cli_capabilities_real_profiles/PLAN.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<!-- task=01_cli_capabilities_real_profiles plan=0 tag=REFACTOR -->
|
||||
|
||||
# CLI Capabilities를 실제 등록된 프로파일만 노출하도록 수정
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/01_cli_capabilities_real_profiles/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
`apps/node/internal/adapters/cli/cli.go:25`의 `knownAgents`는 `claude/gemini/codex/opencode/cline`을 하드코딩으로 들고 있고, `Capabilities`(`cli.go:74-89`)는 이 목록을 `models`에 강제로 추가합니다. 그 결과 사용자가 실제로 등록하지 않은 agent도 capabilities로 광고되며, 그 agent로 실행을 시도하면 `unknown agent` 에러로 실패합니다. capabilities는 어댑터가 실제로 처리 가능한 모델만 반환해야 합니다.
|
||||
|
||||
### [REFACTOR-1] knownAgents 제거 및 Capabilities 단순화
|
||||
|
||||
#### 문제
|
||||
|
||||
`apps/node/internal/adapters/cli/cli.go:24-26`:
|
||||
```go
|
||||
// known agents — must match CLI tool names
|
||||
var knownAgents = []string{"claude", "gemini", "codex", "opencode", "cline"}
|
||||
```
|
||||
|
||||
`apps/node/internal/adapters/cli/cli.go:74-89`의 `Capabilities`가 위 목록을 무조건 `models`에 추가한다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`knownAgents` 변수와 그 처리 루프를 모두 제거하고, `Capabilities.Models`는 `c.profiles`의 키만 정렬해 반환한다.
|
||||
|
||||
```go
|
||||
func (c *CLI) Capabilities(_ context.Context) (runtime.Capabilities, error) {
|
||||
models := make([]string, 0, len(c.profiles))
|
||||
for name := range c.profiles {
|
||||
models = append(models, name)
|
||||
}
|
||||
sort.Strings(models)
|
||||
return runtime.Capabilities{
|
||||
AdapterName: Name,
|
||||
Models: models,
|
||||
MaxConcurrency: 4,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
`MaxConcurrency: 4`는 별도 작업이 없는 한 그대로 둔다(다른 어댑터와 일관).
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/cli.go`에서 `knownAgents` 변수 선언과 주석을 삭제한다.
|
||||
- [ ] `Capabilities` 본체에서 `knownAgents` 루프를 제거하고 정렬된 profile 키만 반환하도록 단순화한다.
|
||||
- [ ] `sort` import는 이미 존재하므로 그대로 사용한다(필요 시 확인).
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`apps/node/internal/adapters/cli/lifecycle_blackbox_test.go`(layout cleanup 후 위치) 또는 현 위치에 다음 케이스 추가:
|
||||
|
||||
- `TestCapabilities_OnlyConfiguredProfiles`: 프로파일을 `{"foo": ..., "bar": ...}`로만 구성한 CLI에 대해 `Capabilities().Models`가 `["bar","foo"]`인지 단언.
|
||||
- `TestCapabilities_EmptyProfilesReturnsEmptyModels`: 빈 profiles에 대해 `len(Models) == 0`인지 단언.
|
||||
|
||||
기존 테스트가 `knownAgents`에 있는 모델을 단언하고 있다면 모두 갱신.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 케이스 포함 PASS.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/lifecycle_blackbox_test.go` (또는 현 위치) | REFACTOR-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./apps/node/...
|
||||
rg -n 'knownAgents' apps/node/
|
||||
```
|
||||
|
||||
예상 결과: build/test PASS, `rg`는 매치 없음.
|
||||
59
agent-task/02_cli_test_layout_cleanup/CODE_REVIEW.md
Normal file
59
agent-task/02_cli_test_layout_cleanup/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<!-- task=02_cli_test_layout_cleanup plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=02_cli_test_layout_cleanup, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] 테스트 파일을 cli 부모 디렉토리로 이동 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 빈 sub-디렉토리(`oneshot`, `persistent`, `lifecycle`)가 모두 제거되었는지
|
||||
- 이동된 4개 파일의 `package` 선언이 모두 `cli_test`인지
|
||||
- 동일 이름의 테스트 헬퍼/함수가 충돌 없이 모두 단일 패키지 안에서 컴파일되는지
|
||||
- `apps/node/internal/adapters/cli/internal/testutil`는 그대로 유지되었는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ ls apps/node/internal/adapters/cli/
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./apps/node/...
|
||||
$ find apps/node/internal/adapters/cli -type d
|
||||
(output)
|
||||
```
|
||||
79
agent-task/02_cli_test_layout_cleanup/PLAN.md
Normal file
79
agent-task/02_cli_test_layout_cleanup/PLAN.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<!-- task=02_cli_test_layout_cleanup plan=0 tag=REFACTOR -->
|
||||
|
||||
# CLI 어댑터 테스트 디렉토리 구조 정리
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/02_cli_test_layout_cleanup/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
`apps/node/internal/adapters/cli/` 아래에 `oneshot/`, `persistent/`, `lifecycle/` 디렉토리가 존재하지만 각 디렉토리에는 `*_test.go` 파일만 들어있고, 실제 구현은 부모 `cli` 패키지의 `oneshot.go`, `persistent.go` 등에 있습니다. 패키지 선언도 `oneshot_test`, `persistent_test`, `lifecycle_test`로 black-box 테스트입니다. 디렉토리 이름이 sub-package의 존재를 암시해 처음 보는 사람이 잘못된 모듈 경계를 추론할 수 있습니다. 구현은 이미 부모 패키지에 모여 있으므로, 테스트 파일도 부모 디렉토리로 옮겨 단일 패키지 black-box 테스트로 통합합니다.
|
||||
|
||||
### [REFACTOR-1] 테스트 파일을 cli 부모 디렉토리로 이동
|
||||
|
||||
#### 문제
|
||||
|
||||
다음 파일들이 의미 없는 sub-디렉토리에 격리되어 있습니다:
|
||||
|
||||
- `apps/node/internal/adapters/cli/oneshot/cli_test.go` (package `oneshot_test`)
|
||||
- `apps/node/internal/adapters/cli/persistent/execute_test.go` (package `persistent_test`)
|
||||
- `apps/node/internal/adapters/cli/persistent/codex_exec_test.go` (package `persistent_test`)
|
||||
- `apps/node/internal/adapters/cli/lifecycle/cli_test.go` (package `lifecycle_test`)
|
||||
|
||||
세 디렉토리 모두 구현 파일 없이 테스트만 들어 있고, 모두 부모 `cli` 패키지를 import해 동작을 검증합니다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
각 테스트 파일을 부모 `apps/node/internal/adapters/cli/` 디렉토리로 이동하고, 패키지 선언을 `cli_test`로 통일합니다. 파일명은 충돌 방지를 위해 다음과 같이 rename합니다:
|
||||
|
||||
| 기존 경로 | 새 경로 |
|
||||
|----------|--------|
|
||||
| `cli/oneshot/cli_test.go` | `cli/oneshot_blackbox_test.go` |
|
||||
| `cli/persistent/execute_test.go` | `cli/persistent_execute_blackbox_test.go` |
|
||||
| `cli/persistent/codex_exec_test.go` | `cli/codex_exec_blackbox_test.go` |
|
||||
| `cli/lifecycle/cli_test.go` | `cli/lifecycle_blackbox_test.go` |
|
||||
|
||||
이동 후 빈 디렉토리(`oneshot/`, `persistent/`, `lifecycle/`)는 삭제합니다. `cli/internal/testutil/`은 외부 패키지에서 import 가능한 그대로 유지합니다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `git mv apps/node/internal/adapters/cli/oneshot/cli_test.go apps/node/internal/adapters/cli/oneshot_blackbox_test.go`
|
||||
- [ ] `git mv apps/node/internal/adapters/cli/persistent/execute_test.go apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go`
|
||||
- [ ] `git mv apps/node/internal/adapters/cli/persistent/codex_exec_test.go apps/node/internal/adapters/cli/codex_exec_blackbox_test.go`
|
||||
- [ ] `git mv apps/node/internal/adapters/cli/lifecycle/cli_test.go apps/node/internal/adapters/cli/lifecycle_blackbox_test.go`
|
||||
- [ ] 이동된 4개 파일의 `package` 선언을 `cli_test`로 변경한다.
|
||||
- [ ] 빈 디렉토리 `apps/node/internal/adapters/cli/oneshot/`, `persistent/`, `lifecycle/`를 `rmdir`로 제거한다.
|
||||
- [ ] 같은 이름의 헬퍼/심볼이 두 파일에 동시 존재하면 prefix(`oneshot_`, `persistent_` 등)로 rename해 충돌을 푼다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
새 테스트는 추가하지 않는다. 이 항목은 파일 이동/패키지 명 변경만 수행하며, 기존 테스트 케이스가 그대로 동작하는 것이 회귀 검증 자체이다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
ls apps/node/internal/adapters/cli/
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: `oneshot/`, `persistent/`, `lifecycle/` 디렉토리가 사라지고, 4개 `*_blackbox_test.go`가 부모에 있다. `go test`는 단일 패키지 `iop/apps/node/internal/adapters/cli`에서 모두 PASS.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/node/internal/adapters/cli/oneshot_blackbox_test.go` (이동/리네임) | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` (이동/리네임) | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/codex_exec_blackbox_test.go` (이동/리네임) | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/lifecycle_blackbox_test.go` (이동/리네임) | REFACTOR-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./apps/node/...
|
||||
find apps/node/internal/adapters/cli -type d
|
||||
```
|
||||
|
||||
예상 결과: build/test 모두 PASS. `find` 결과는 `apps/node/internal/adapters/cli`, `apps/node/internal/adapters/cli/internal`, `apps/node/internal/adapters/cli/internal/testutil`만 보인다.
|
||||
74
agent-task/03_cli_emitter_interface/CODE_REVIEW.md
Normal file
74
agent-task/03_cli_emitter_interface/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!-- task=03_cli_emitter_interface plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=03_cli_emitter_interface, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log`
|
||||
2. `PLAN.md` → `plan_M.log`
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] lineEmitter 인터페이스와 공통 driveJSONLines 도입 | [ ] |
|
||||
| [REFACTOR-2] 5개 포맷을 인터페이스 구현으로 이전 | [ ] |
|
||||
| [REFACTOR-3] executeCommand switch를 registry 조회로 단순화 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 5개 emit* 함수가 모두 삭제되고 oneshot.go가 명시적으로 짧아졌는지 (`wc -l`로 확인)
|
||||
- `driveJSONLines`가 RunID/Timestamp 자동 채움을 책임지고 emitter 구조체에는 그 정보가 새지 않는지
|
||||
- 알 수 없는 `OutputFormat`에 대한 fallback이 raw stdout chunks로 일관되는지(또는 결정된 정책대로 동작하는지)
|
||||
- 큰 라인이 필요한 claude/cline 포맷에서 `scanBufMax`가 8MB로 유지되는지
|
||||
- raw `emitStdoutChunks`는 인터페이스에서 제외된 채 그대로 동작하는지
|
||||
- 회귀 테스트(black-box)가 모두 PASS이며 신규 emitter 단위 테스트가 추가됐는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-2 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-3 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./apps/node/...
|
||||
$ wc -l apps/node/internal/adapters/cli/oneshot.go apps/node/internal/adapters/cli/emitters.go
|
||||
(output)
|
||||
```
|
||||
233
agent-task/03_cli_emitter_interface/PLAN.md
Normal file
233
agent-task/03_cli_emitter_interface/PLAN.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
<!-- task=03_cli_emitter_interface plan=0 tag=REFACTOR -->
|
||||
|
||||
# CLI oneshot output emitter 인터페이스 추출
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/03_cli_emitter_interface/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
`apps/node/internal/adapters/cli/oneshot.go`는 5개 JSONL 포맷별 emitter — `emitStreamJSONLines`, `emitClaudeJSONLines`, `emitCodexJSONLines`, `emitOpencodeJSON`, `emitClineJSON` — 와 raw `emitStdoutChunks`를 모두 포함해 481줄로 비대화되어 있습니다. 5개 함수 모두 동일한 보일러플레이트(scanner buffer setup, outBuf 누적, JSON unmarshal, sink emit, scanner err 검사)를 반복하고 포맷별로 라인 → `RuntimeEvent` 변환만 다릅니다. 인터페이스 도입 기준은 "구현체 두 개 이상"이며, 현재 6개이므로 충분히 추상화 대상입니다. 라인-기반 변환만 분리하고, scanner 루프와 공통 처리는 한 곳으로 묶습니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. `[REFACTOR-1]` `lineEmitter` 인터페이스와 공통 scanner 드라이버를 정의.
|
||||
2. `[REFACTOR-2]` 5개 JSON 포맷을 인터페이스 구현으로 이전.
|
||||
3. `[REFACTOR-3]` `executeCommand`의 switch가 emitter registry를 거치도록 단순화.
|
||||
|
||||
### [REFACTOR-1] lineEmitter 인터페이스와 공통 드라이버 정의
|
||||
|
||||
#### 문제
|
||||
|
||||
`oneshot.go:135-469`에 동일한 scanner 루프가 다섯 번 반복된다. 새 포맷이 들어오면 같은 50-60줄 보일러플레이트를 한 번 더 복사하게 된다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
새 파일 `apps/node/internal/adapters/cli/emitters.go`에 다음을 정의한다:
|
||||
|
||||
```go
|
||||
// lineEmitter parses one stdout line (already trimmed of trailing newline) and
|
||||
// returns the resulting RuntimeEvents to push to the sink. Empty events are
|
||||
// skipped. Returning a non-nil error aborts the run.
|
||||
type lineEmitter interface {
|
||||
Name() string
|
||||
Emit(line string) ([]runtime.RuntimeEvent, error)
|
||||
}
|
||||
|
||||
// driveJSONLines runs a shared scanner loop over stdout, accumulates raw output
|
||||
// into outBuf, dispatches each line through the emitter, and forwards events
|
||||
// to sink. Returns total OutputTokens approximation.
|
||||
func driveJSONLines(
|
||||
ctx context.Context,
|
||||
stdout io.Reader,
|
||||
sink runtime.EventSink,
|
||||
runID string,
|
||||
outBuf *strings.Builder,
|
||||
emitter lineEmitter,
|
||||
scanBufMax int,
|
||||
) (int, error) {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
scanner.Buffer(make([]byte, 64*1024), scanBufMax)
|
||||
outputTokens := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
outBuf.Write(line)
|
||||
outBuf.WriteByte('\n')
|
||||
trimmed := strings.TrimSpace(string(line))
|
||||
if trimmed == "" || trimmed[0] != '{' {
|
||||
continue
|
||||
}
|
||||
events, err := emitter.Emit(trimmed)
|
||||
if err != nil {
|
||||
return outputTokens, err
|
||||
}
|
||||
for _, ev := range events {
|
||||
ev.RunID = runID
|
||||
if ev.Timestamp.IsZero() {
|
||||
ev.Timestamp = time.Now()
|
||||
}
|
||||
if ev.Type == runtime.EventTypeDelta {
|
||||
outputTokens += len(strings.Fields(ev.Delta))
|
||||
}
|
||||
_ = sink.Emit(ctx, ev)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return outputTokens, err
|
||||
}
|
||||
return outputTokens, nil
|
||||
}
|
||||
```
|
||||
|
||||
raw `emitStdoutChunks`는 인터페이스 대상이 아니므로 그대로 둔다(JSON 라인 모델이 아님).
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] 새 파일 `apps/node/internal/adapters/cli/emitters.go` 생성, `lineEmitter`/`driveJSONLines` 정의.
|
||||
- [ ] `apps/node/internal/adapters/cli/oneshot.go`의 `emitStdoutChunks`는 그대로 둔다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
신규 단위 테스트 `apps/node/internal/adapters/cli/emitters_internal_test.go` 또는 black-box 테스트:
|
||||
|
||||
- `TestDriveJSONLines_DispatchesEmitterEvents`: 줄 두 개를 흘려보내고 emitter가 반환한 이벤트가 sink에 전달되며 RunID/Timestamp가 채워지는지 단언.
|
||||
- `TestDriveJSONLines_StopsOnEmitterError`: emitter가 error 반환 시 즉시 루프 종료.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 테스트 PASS.
|
||||
|
||||
### [REFACTOR-2] 5개 포맷을 인터페이스 구현으로 이전
|
||||
|
||||
#### 문제
|
||||
|
||||
`oneshot.go`의 5개 emit* 함수가 각자 scanner 루프 + JSON 디코드 + sink emit을 직접 한다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
각 포맷에 대해 line-only 변환 구현을 만든다 (예: `streamJSONEmitter`, `claudeJSONEmitter`, `codexJSONEmitter`, `opencodeJSONEmitter`, `clineJSONEmitter`). 각 구조체는 기존 anonymous JSON struct 정의를 그대로 들고, `Emit(line string) ([]runtime.RuntimeEvent, error)` 시그니처로 라인 한 줄을 처리해 0~N개의 `RuntimeEvent`를 반환한다.
|
||||
|
||||
예시 — codex:
|
||||
|
||||
```go
|
||||
type codexJSONEmitter struct{}
|
||||
func (codexJSONEmitter) Name() string { return "codex-json" }
|
||||
func (codexJSONEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) {
|
||||
var ev struct{ ... } // 기존 구조 재사용
|
||||
if err := json.Unmarshal([]byte(line), &ev); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
switch ev.Type {
|
||||
case "item.completed":
|
||||
if ev.Item.Type == "agent_message" && ev.Item.Text != "" {
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: ev.Item.Text}}, nil
|
||||
}
|
||||
case "error":
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeError, Error: ev.Message}}, nil
|
||||
case "turn.failed":
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeError, Error: ev.Error.Message}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
```
|
||||
|
||||
기존 `emitStreamJSONLines`/`emitClaudeJSONLines`/... 함수는 모두 삭제한다. 일부 emitter는 더 큰 scanner buffer가 필요(claude 8MB, cline 8MB)하므로 `lineEmitter`에 `MaxLineBytes() int` 옵션 메서드를 추가하거나, 등록 시 `scanBufMax`를 함께 명시한다. 본 계획은 등록 형태로 처리:
|
||||
|
||||
```go
|
||||
type registeredEmitter struct {
|
||||
emitter lineEmitter
|
||||
scanBufMax int
|
||||
}
|
||||
|
||||
var jsonEmitters = map[string]registeredEmitter{
|
||||
"stream-json": {streamJSONEmitter{}, 4 * 1024 * 1024},
|
||||
"claude-json": {claudeJSONEmitter{}, 8 * 1024 * 1024},
|
||||
"codex-json": {codexJSONEmitter{}, 4 * 1024 * 1024},
|
||||
"opencode-json": {opencodeJSONEmitter{}, 4 * 1024 * 1024},
|
||||
"cline-json": {clineJSONEmitter{}, 8 * 1024 * 1024},
|
||||
}
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/emitters.go`에 5개 emitter 구조체와 `jsonEmitters` registry를 정의한다.
|
||||
- [ ] `apps/node/internal/adapters/cli/oneshot.go`에서 `emitStreamJSONLines`, `emitClaudeJSONLines`, `emitCodexJSONLines`, `emitOpencodeJSON`, `emitClineJSON`을 삭제한다.
|
||||
- [ ] 더 이상 필요 없는 `bufio`, `encoding/json`, `errors` import는 oneshot.go에서 제거(사용처 확인 필요).
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
기존 black-box 테스트(`oneshot_blackbox_test.go` 또는 현재 `oneshot/cli_test.go`)가 5개 포맷의 end-to-end 동작을 검증하므로 회귀 검증으로 충분. 추가로 emitter 단위 테스트:
|
||||
|
||||
- `TestStreamJSONEmitter_AssistantMessageBecomesDelta`
|
||||
- `TestClaudeJSONEmitter_TextDelta`
|
||||
- `TestCodexJSONEmitter_TurnFailedBecomesError`
|
||||
- `TestOpencodeJSONEmitter_TextPart`
|
||||
- `TestClineJSONEmitter_AskApiReqFailedBecomesError`
|
||||
|
||||
각 테스트는 한 줄을 입력으로 주고 반환되는 `RuntimeEvent` 슬라이스의 타입/필드를 단언한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: 기존 케이스 + 신규 emitter 단위 테스트 PASS.
|
||||
|
||||
### [REFACTOR-3] executeCommand의 switch를 registry 조회로 단순화
|
||||
|
||||
#### 문제
|
||||
|
||||
`oneshot.go:67-80`의 `switch profile.OutputFormat`은 분기마다 emit 함수 호출 시그니처를 반복한다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
```go
|
||||
var outputTokens int
|
||||
var readErr error
|
||||
if reg, ok := jsonEmitters[profile.OutputFormat]; ok {
|
||||
outputTokens, readErr = driveJSONLines(ctx, stdout, sink, spec.RunID, &outBuf, reg.emitter, reg.scanBufMax)
|
||||
} else {
|
||||
outputTokens, readErr = emitStdoutChunks(ctx, stdout, sink, spec.RunID, &outBuf)
|
||||
}
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/oneshot.go`의 `executeCommand`에서 switch를 위 형태로 교체한다.
|
||||
- [ ] 알 수 없는 `OutputFormat`이 들어오면 `emitStdoutChunks`로 fallback (현 동작 유지) 또는 명시 에러 반환 중 하나로 결정해 코드와 docstring에 명시한다. 본 계획은 fallback 유지.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
추가 테스트 없음. `oneshot_blackbox_test.go`의 기존 케이스가 회귀 검증.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: PASS.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/node/internal/adapters/cli/emitters.go` (신규) | REFACTOR-1, REFACTOR-2 |
|
||||
| `apps/node/internal/adapters/cli/oneshot.go` | REFACTOR-2, REFACTOR-3 |
|
||||
| `apps/node/internal/adapters/cli/emitters_internal_test.go` (신규) 또는 외부 테스트 | REFACTOR-1, REFACTOR-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./apps/node/...
|
||||
wc -l apps/node/internal/adapters/cli/oneshot.go apps/node/internal/adapters/cli/emitters.go
|
||||
```
|
||||
|
||||
예상 결과: 모든 테스트 PASS, `oneshot.go`가 200줄 이하로 축소되고 emitter 보일러플레이트가 한 곳으로 모인다.
|
||||
58
agent-task/04_cli_persistent_cancel_reason/CODE_REVIEW.md
Normal file
58
agent-task/04_cli_persistent_cancel_reason/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<!-- task=04_cli_persistent_cancel_reason plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=04_cli_persistent_cancel_reason, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log`
|
||||
2. `PLAN.md` → `plan_M.log`
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] context 종료 사유를 cancel 이벤트 Message로 분리 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `cancelEventForContext`가 `context.Canceled` / `context.DeadlineExceeded`를 정확히 구분하는지
|
||||
- persistent와 oneshot 양쪽 cancel 분기에서 동일 헬퍼를 사용해 일관된 문자열을 emit하는지
|
||||
- cancel emit이 base context로 이뤄져 이미 종료된 ctx 때문에 누락되지 않는지
|
||||
- 새 메시지 값이 edge console의 인쇄/필터에 회귀를 일으키지 않는지(`apps/edge/cmd/edge/console.go:212-215` 확인)
|
||||
- cancel 후에도 `runtime.ErrRunCancelled` 반환이 유지되어 `node.completeRun`의 status 매핑이 깨지지 않는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./apps/node/...
|
||||
(output)
|
||||
```
|
||||
107
agent-task/04_cli_persistent_cancel_reason/PLAN.md
Normal file
107
agent-task/04_cli_persistent_cancel_reason/PLAN.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<!-- task=04_cli_persistent_cancel_reason plan=0 tag=REFACTOR -->
|
||||
|
||||
# Persistent 실행 cancel 사유(timeout vs user-cancel) 구분
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/04_cli_persistent_cancel_reason/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
`apps/node/internal/adapters/cli/persistent.go:65-74`의 `<-ctx.Done()` 분기와 `apps/node/internal/adapters/cli/oneshot.go:96-104`의 동일 분기는 timeout으로 인한 컨텍스트 종료와 사용자 cancel을 구분하지 않고 모두 `EventTypeCancelled` + `Message: "cli execution cancelled"`로 보고합니다. 운영 시 두 사유는 대응이 다르므로 (timeout은 설정 조정, user-cancel은 워크플로 변경) `Message`/`Error` 필드에 사유를 명시해 구분 가능하게 합니다.
|
||||
|
||||
### [REFACTOR-1] context 종료 사유를 cancel 이벤트에 반영
|
||||
|
||||
#### 문제
|
||||
|
||||
- `persistent.go:65-74`: `case <-ctx.Done():`에서 항상 `Message: "cli execution cancelled"`로 emit.
|
||||
- `oneshot.go:96-103`: 동일하게 `Message: "cli execution cancelled"`.
|
||||
|
||||
`context.Canceled`와 `context.DeadlineExceeded`를 구분하지 않는다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
각 분기에서 `ctx.Err()`를 보고 사유를 분기한다.
|
||||
|
||||
```go
|
||||
func cancelEventForContext(err error) (msg string) {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "timeout"
|
||||
case errors.Is(err, context.Canceled):
|
||||
return "user-cancel"
|
||||
default:
|
||||
return "context-done"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`persistent.go`:
|
||||
|
||||
```go
|
||||
case <-ctx.Done():
|
||||
drainSessionUntilIdle(sess.output, idleTimeout, c.logger, sess.key)
|
||||
_ = sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: spec.RunID,
|
||||
Type: runtime.EventTypeCancelled,
|
||||
Message: cancelEventForContext(ctx.Err()),
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
return runtime.ErrRunCancelled
|
||||
```
|
||||
|
||||
`oneshot.go`의 `cmd.Wait()` 실패 후 `if ctx.Err() != nil` 분기도 동일하게:
|
||||
|
||||
```go
|
||||
_ = sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: spec.RunID,
|
||||
Type: runtime.EventTypeCancelled,
|
||||
Message: cancelEventForContext(ctx.Err()),
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
return combinedOutput(), runtime.ErrRunCancelled
|
||||
```
|
||||
|
||||
`cancelEventForContext` 헬퍼는 `apps/node/internal/adapters/cli/cli.go` 또는 새 파일 `apps/node/internal/adapters/cli/cancel.go`에 둔다. 본 계획은 `cli.go`에 둔다.
|
||||
|
||||
> 호환성: edge console (`apps/edge/cmd/edge/console.go:212-215`)이 cancel 이벤트의 `Message`를 인쇄에만 사용하므로 동작 깨짐 없음. 다만 노출되는 텍스트가 바뀌므로 사용자 안내 문서/리드미는 따로 갱신하지 않는다(현재 README가 해당 문구를 명시하지 않음).
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/cli.go`에 `cancelEventForContext` 헬퍼를 추가한다.
|
||||
- [ ] `apps/node/internal/adapters/cli/persistent.go`의 `ctx.Done()` 분기 `Message`를 헬퍼 호출로 교체한다.
|
||||
- [ ] `apps/node/internal/adapters/cli/oneshot.go`의 cancel emit `Message`를 헬퍼 호출로 교체한다.
|
||||
- [ ] `errors`/`context` import가 두 파일에 모두 있는지 확인한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
- `TestCancelEventForContext_DeadlineMapsToTimeout`: `cancelEventForContext(context.DeadlineExceeded) == "timeout"` 단언.
|
||||
- `TestCancelEventForContext_CanceledMapsToUserCancel`: `context.Canceled` → `"user-cancel"` 단언.
|
||||
- `TestExecutePersistent_TimeoutEmitsTimeoutMessage`: 짧은 deadline ctx로 persistent 실행 → 마지막 cancel 이벤트의 `Message == "timeout"` 단언.
|
||||
- `TestExecuteOneShot_UserCancelEmitsUserCancelMessage`: 명시적 `cancel()` 호출 후 cancel 이벤트 `Message == "user-cancel"` 단언.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 케이스 PASS.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/persistent.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/oneshot.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/lifecycle_blackbox_test.go` 또는 적절한 위치 | REFACTOR-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./apps/node/...
|
||||
```
|
||||
|
||||
예상 결과: 모든 테스트 PASS, cancel 이벤트의 `Message`가 `timeout` / `user-cancel` / `context-done` 셋 중 하나로 emit됨.
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<!-- task=05_cli_persistent_explicit_complete plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=05_cli_persistent_explicit_complete, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log`
|
||||
2. `PLAN.md` → `plan_M.log`
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] CLIProfileConf에 completion_marker 추가 | [ ] |
|
||||
| [REFACTOR-2] persistent 루프에서 마커 매칭으로 명시적 complete | [ ] |
|
||||
| [REFACTOR-3] idle-timeout fallback의 사유 표시 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `CompletionMarkerConf.Empty()`가 line/regex 둘 다 비어 있을 때만 true를 반환하는지
|
||||
- `newCompletionMatcher`가 잘못된 정규식을 명시 에러로 반환하고, `executePersistent` 진입부에서 `emitRuntimeError`로 보고하는지
|
||||
- 마커가 없는 프로파일에서 idle-timeout 경로가 그대로 동작하고, `Message == "idle-timeout"`로 emit하는지
|
||||
- 마커 매칭 시 즉시 종료되며 추가 출력이 emit되지 않는지 (race 단언 포함)
|
||||
- `completion_marker.line` 매칭이 trailing whitespace로 인해 깨지지 않는지(필요 시 trim 정책 명시)
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ go test ./packages/config/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-2 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-3 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/... -run Persistent
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./...
|
||||
(output)
|
||||
```
|
||||
218
agent-task/05_cli_persistent_explicit_complete/PLAN.md
Normal file
218
agent-task/05_cli_persistent_explicit_complete/PLAN.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<!-- task=05_cli_persistent_explicit_complete plan=0 tag=REFACTOR -->
|
||||
|
||||
# Persistent CLI 세션의 명시적 종료 이벤트 도입
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/05_cli_persistent_explicit_complete/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
`apps/node/internal/adapters/cli/persistent.go:104-114`의 persistent 분기는 출력이 `idleTimeout`(기본 1500ms) 동안 도착하지 않으면 응답이 끝났다고 판정해 `EventTypeComplete`를 emit합니다. 모델이 잠시 멈추는 경우 false-complete가 발생할 수 있고, 응답이 도중에 잘려도 사용자는 알 수 없습니다. 본 작업은 가능한 곳은 명시적 종료 마커를 사용해 응답 종료를 확실히 검출하고, 마커가 없는 CLI에 대해서만 idle-timeout fallback을 유지하는 구조로 바꿉니다.
|
||||
|
||||
이번 작업은 다음 두 가지를 포함합니다.
|
||||
|
||||
1. CLI 프로파일에 `completion_marker` 명세를 도입하고, persistent 어댑터가 그 마커를 보면 즉시 `complete`를 emit하도록 한다.
|
||||
2. 마커가 정의되지 않은 프로파일은 기존 idle-timeout 동작을 유지하되, 발생 시 `EventTypeComplete`의 `Message`에 `"idle-timeout"` 사유를 명시해 운영 로그에서 구분 가능하게 한다.
|
||||
|
||||
> 정합성 메모: 현재 사용 중인 persistent 프로파일의 종료 마커를 직접 확인할 수 없는 경우, fallback 경로(idle-timeout + reason)를 항상 동작 가능하게 유지한다. 가능한 마커 후보는 `completion_marker.line`(정확 일치) 와 `completion_marker.regex`(정규식) 둘 다 지원한다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. `[REFACTOR-1]` `CLIProfileConf`에 `completion_marker` 구성 추가.
|
||||
2. `[REFACTOR-2]` persistent 실행 루프에 마커 매칭 분기 도입.
|
||||
3. `[REFACTOR-3]` idle-timeout fallback에서 사유를 명시.
|
||||
|
||||
### [REFACTOR-1] CLI 프로파일에 completion_marker 추가
|
||||
|
||||
#### 문제
|
||||
|
||||
`packages/config/config.go:106-119`의 `CLIProfileConf`에는 응답 종료를 식별할 명시적 필드가 없다. 현재 persistent 종료는 `ResponseIdleTimeoutMS` 단일 신호만 사용한다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
```go
|
||||
type CLIProfileConf struct {
|
||||
// 기존 필드들...
|
||||
CompletionMarker CompletionMarkerConf `mapstructure:"completion_marker" yaml:"completion_marker"`
|
||||
}
|
||||
|
||||
type CompletionMarkerConf struct {
|
||||
Line string `mapstructure:"line" yaml:"line"` // exact-match termination line
|
||||
Regex string `mapstructure:"regex" yaml:"regex"` // regex termination match
|
||||
}
|
||||
|
||||
func (m CompletionMarkerConf) Empty() bool { return m.Line == "" && m.Regex == "" }
|
||||
```
|
||||
|
||||
YAML 예시(추가 필요할 때만 사용):
|
||||
|
||||
```yaml
|
||||
my-agent:
|
||||
completion_marker:
|
||||
line: "<<END_OF_RESPONSE>>"
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `packages/config/config.go`에 `CompletionMarkerConf` 타입과 `CLIProfileConf.CompletionMarker` 필드를 추가한다.
|
||||
- [ ] `packages/config/config_test.go`에 yaml 입력에서 `completion_marker.line` / `completion_marker.regex` 모두 정상 unmarshal되는 케이스를 추가한다.
|
||||
- [ ] `cli_profile_proto_message` 작업이 머지된 후라면 `proto/iop/runtime.proto`의 `CLIProfileConfig`에도 동일 필드(중첩 메시지)를 추가하고 `make proto`.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`TestCompletionMarkerConf_RegexAndLine`: line/regex 둘 다 채워진 경우, 둘 다 비어있는 경우(`Empty() == true`)를 단언.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./packages/config/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 케이스 PASS.
|
||||
|
||||
### [REFACTOR-2] persistent 루프에서 명시적 마커 매칭
|
||||
|
||||
#### 문제
|
||||
|
||||
`apps/node/internal/adapters/cli/persistent.go:75-103` (`case out, ok := <-sess.output:`) 에서 매 출력마다 idle 타이머만 갱신한다. 마커 라인이 들어와도 인식하지 못하고, 다음 idle window가 도래해야만 `complete`를 emit한다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
매 출력 라인을 emit한 직후 `profile.CompletionMarker`와 매칭해 명시적 종료를 결정한다. 매처는 한 번만 컴파일해 세션에 캐시한다.
|
||||
|
||||
```go
|
||||
type completionMatcher struct {
|
||||
line string
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func newCompletionMatcher(m config.CompletionMarkerConf) (completionMatcher, error) {
|
||||
var cm completionMatcher
|
||||
cm.line = m.Line
|
||||
if m.Regex != "" {
|
||||
re, err := regexp.Compile(m.Regex)
|
||||
if err != nil {
|
||||
return completionMatcher{}, fmt.Errorf("completion_marker regex: %w", err)
|
||||
}
|
||||
cm.re = re
|
||||
}
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
func (m completionMatcher) match(line string) bool {
|
||||
if m.line != "" && line == m.line {
|
||||
return true
|
||||
}
|
||||
if m.re != nil && m.re.MatchString(line) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
`executePersistent` 본체:
|
||||
|
||||
```go
|
||||
matcher, err := newCompletionMatcher(profile.CompletionMarker)
|
||||
if err != nil {
|
||||
return emitRuntimeError(ctx, sink, spec.RunID, err.Error())
|
||||
}
|
||||
```
|
||||
|
||||
`case out, ok := <-sess.output:` 분기에서 emit 직후:
|
||||
|
||||
```go
|
||||
if matcher.match(out.line) {
|
||||
return sink.Emit(ctx, runtime.RuntimeEvent{
|
||||
RunID: spec.RunID,
|
||||
Type: runtime.EventTypeComplete,
|
||||
Message: "completion-marker",
|
||||
Usage: &runtime.UsageStats{InputTokens: len(strings.Fields(prompt)), OutputTokens: outputTokens},
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
마커가 비어있는 프로파일(`matcher.line == "" && matcher.re == nil`)은 `match`가 항상 false를 반환하므로 기존 idle-timeout 경로가 그대로 동작한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/persistent.go`에 `completionMatcher`와 `newCompletionMatcher`를 추가한다.
|
||||
- [ ] `executePersistent` 진입부에서 matcher를 한 번 빌드하고, 출력 분기에서 매칭한다.
|
||||
- [ ] `regexp` import를 추가한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` (현 위치 또는 layout cleanup 후 위치)에 다음 케이스 추가:
|
||||
|
||||
- `TestExecutePersistent_CompletionMarkerLineEndsRun`: stub CLI가 `"<<END>>"` 라인을 보내면 즉시 `EventTypeComplete{Message:"completion-marker"}`이 emit되는지 단언.
|
||||
- `TestExecutePersistent_CompletionMarkerRegexEndsRun`: `^DONE \d+$` 정규식 기반 마커가 일치하면 종료되는지 단언.
|
||||
- `TestExecutePersistent_NoMarkerFallsBackToIdleTimeout`: 마커가 비어있을 때 기존 idle-timeout 경로가 그대로 PASS인지 단언.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 케이스 포함 PASS.
|
||||
|
||||
### [REFACTOR-3] idle-timeout fallback의 사유 표시
|
||||
|
||||
#### 문제
|
||||
|
||||
`persistent.go:104-114`의 `<-idleC` 분기는 `Message: "cli execution complete"`로 emit하므로, 명시적 마커로 끝난 경우와 idle 추정으로 끝난 경우를 운영 로그에서 구분할 수 없다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
idle 분기의 `Message`를 `"idle-timeout"`으로 변경한다. 마커 매칭 분기는 `"completion-marker"`를 사용한다.
|
||||
|
||||
```go
|
||||
case <-idleC:
|
||||
return sink.Emit(ctx, runtime.RuntimeEvent{
|
||||
RunID: spec.RunID,
|
||||
Type: runtime.EventTypeComplete,
|
||||
Message: "idle-timeout",
|
||||
Usage: &runtime.UsageStats{
|
||||
InputTokens: len(strings.Fields(prompt)),
|
||||
OutputTokens: outputTokens,
|
||||
},
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/persistent.go`의 idle 분기 `Message`를 `"idle-timeout"`으로 변경한다.
|
||||
- [ ] 기존 단위 테스트 중 `Message`를 단언하던 케이스가 있다면 같이 갱신한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`TestExecutePersistent_IdleTimeoutMessageReason`: idle 분기로 종료될 때 `event.Message == "idle-timeout"`임을 단언.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/... -run Persistent
|
||||
```
|
||||
|
||||
예상 결과: PASS.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `packages/config/config.go` | REFACTOR-1 |
|
||||
| `packages/config/config_test.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/persistent.go` | REFACTOR-2, REFACTOR-3 |
|
||||
| `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` (또는 현 위치) | REFACTOR-2, REFACTOR-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
예상 결과: 모든 테스트 PASS, persistent 종료 사유가 marker/idle-timeout으로 구분 가능.
|
||||
72
agent-task/06_cli_codex_explicit_mode/CODE_REVIEW.md
Normal file
72
agent-task/06_cli_codex_explicit_mode/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<!-- task=06_cli_codex_explicit_mode plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=06_cli_codex_explicit_mode, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log`
|
||||
2. `PLAN.md` → `plan_M.log`
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] CLIProfileConf에 Mode/ResumeArgs 추가 | [ ] |
|
||||
| [REFACTOR-2] CLI 어댑터를 Mode 기반 분기로 전환 | [ ] |
|
||||
| [REFACTOR-3] edge.yaml codex 프로파일 갱신 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `isCodexExecProfile`, `codexResumeOptions`, `path/filepath` import가 모두 제거되었는지
|
||||
- `Execute`/`Start`/`TerminateSession` 모두 `profile.Mode == "codex-exec"` 단일 조건으로 분기하는지
|
||||
- `ResumeArgs == nil` 가드가 명시적 에러를 반환하는지
|
||||
- `configs/edge.yaml`의 codex 프로파일이 `persistent: false` + `mode: codex-exec` 조합인지
|
||||
- `cli_profile_proto_message`와 머지 충돌 가능성 (양쪽 작업이 모두 있을 경우 proto 필드 동기화 여부)
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ go test ./packages/config/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-2 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-3 중간 검증
|
||||
```
|
||||
$ rg -n 'mode: "codex-exec"|resume_args:' configs/edge.yaml
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./...
|
||||
(output)
|
||||
```
|
||||
200
agent-task/06_cli_codex_explicit_mode/PLAN.md
Normal file
200
agent-task/06_cli_codex_explicit_mode/PLAN.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<!-- task=06_cli_codex_explicit_mode plan=0 tag=REFACTOR -->
|
||||
|
||||
# Codex exec 분기를 명시적 mode/resume_args 필드로 전환
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/06_cli_codex_explicit_mode/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
`apps/node/internal/adapters/cli/codex_exec.go:20`의 `isCodexExecProfile`은 `filepath.Base(profile.Command) == "codex" && profile.Args[0] == "exec"`로 codex exec 프로파일을 추정합니다. command 이름과 첫 인자에 의존하는 sniffing은 사용자 입장에서 우회 동작 여부가 보이지 않고, codex CLI 사용 패턴이 바뀌면 잘못된 분기가 됩니다. 또한 `codexResumeOptions`(`codex_exec.go:79-105`)는 codex CLI의 옵션 화이트/블랙리스트를 하드코딩해 새 옵션이 추가될 때마다 깨질 위험이 있습니다. 두 가지 모두 `CLIProfileConf`에 명시 필드를 추가해 의도를 코드가 아닌 설정에서 표현합니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. `[REFACTOR-1]` `CLIProfileConf`에 `Mode`, `ResumeArgs` 필드 추가 + 기본값/YAML/proto 동기화.
|
||||
2. `[REFACTOR-2]` `cli` 어댑터의 `Execute` 분기와 `executeCodexExec`가 `Mode`/`ResumeArgs`를 사용하도록 변경. command sniffing 제거.
|
||||
3. `[REFACTOR-3]` `configs/edge.yaml`의 `codex` 프로파일이 새 필드를 사용하도록 갱신. 회귀 테스트 갱신.
|
||||
|
||||
> 주: `cli_profile_proto_message` 작업이 먼저 머지되면 proto 동기화는 `CLIProfileConfig`에 두 필드를 추가하는 형태가 된다. 두 작업이 독립으로 진행될 경우, 본 작업은 `mapstructure` 필드만 추가하고 proto 작업이 들어올 때 함께 갱신.
|
||||
|
||||
### [REFACTOR-1] 설정 스키마에 Mode/ResumeArgs 필드 추가
|
||||
|
||||
#### 문제
|
||||
|
||||
`packages/config/config.go:106-119`의 `CLIProfileConf`는 codex exec 동작을 표현할 명시적 필드가 없어 어댑터가 command 이름을 sniff하게 만든다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
```go
|
||||
type CLIProfileConf struct {
|
||||
Command string `mapstructure:"command" yaml:"command"`
|
||||
Args []string `mapstructure:"args" yaml:"args"`
|
||||
Env []string `mapstructure:"env" yaml:"env"`
|
||||
Persistent bool `mapstructure:"persistent" yaml:"persistent"`
|
||||
Terminal bool `mapstructure:"terminal" yaml:"terminal"`
|
||||
ResponseIdleTimeoutMS int `mapstructure:"response_idle_timeout_ms" yaml:"response_idle_timeout_ms"`
|
||||
StartupIdleTimeoutMS int `mapstructure:"startup_idle_timeout_ms" yaml:"startup_idle_timeout_ms"`
|
||||
OutputFormat string `mapstructure:"output_format" yaml:"output_format"`
|
||||
// Mode declares the execution dialect. "" means default (oneshot/persistent
|
||||
// based on Persistent). "codex-exec" means use codex exec resume per session.
|
||||
Mode string `mapstructure:"mode" yaml:"mode"`
|
||||
// ResumeArgs is the explicit args list to use when reissuing the command
|
||||
// for an existing session. Required when Mode == "codex-exec".
|
||||
ResumeArgs []string `mapstructure:"resume_args" yaml:"resume_args"`
|
||||
}
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `packages/config/config.go`에 `Mode`, `ResumeArgs` 필드를 추가한다.
|
||||
- [ ] `packages/config/config_test.go`에 두 필드가 YAML에서 정상 로드되는지 단언하는 케이스를 1개 추가한다.
|
||||
- [ ] `cli_profile_proto_message` 작업이 머지된 후라면 `proto/iop/runtime.proto`의 `CLIProfileConfig`에 `mode`, `resume_args`를 추가하고 `make proto`. (그렇지 않으면 본 작업의 검증은 yaml 경로만 확인하고, 후속 작업에서 동기화한다.)
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`packages/config/config_test.go`에 `TestLoadEdge_CLIProfileMode` (가칭): yaml에 `mode: codex-exec`, `resume_args: ["exec","resume","--json"]`이 들어있을 때 unmarshal 결과가 일치하는지 단언.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./packages/config/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 케이스 포함 PASS.
|
||||
|
||||
### [REFACTOR-2] CLI 어댑터를 mode/ResumeArgs 기반 분기로 전환
|
||||
|
||||
#### 문제
|
||||
|
||||
- `apps/node/internal/adapters/cli/cli.go:160-167`의 `Execute`가 `isCodexExecProfile`을 호출.
|
||||
- `apps/node/internal/adapters/cli/codex_exec.go:20-22`의 sniffing 로직.
|
||||
- `apps/node/internal/adapters/cli/codex_exec.go:67-105`의 `codexExecArgs`/`codexResumeOptions`가 하드코딩 옵션 처리.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`Execute`의 분기를 다음과 같이 단순화:
|
||||
|
||||
```go
|
||||
if profile.Mode == "codex-exec" {
|
||||
return c.executeCodexExec(ctx, spec, profile, sink)
|
||||
}
|
||||
if profile.Persistent {
|
||||
return c.executePersistent(ctx, spec, profile, sink)
|
||||
}
|
||||
return c.executeOneShot(ctx, spec, profile, sink)
|
||||
```
|
||||
|
||||
`Start` 루프(`cli.go:93-119`)에서도 `profile.Mode == "codex-exec"`인 프로파일은 persistent worker를 미리 띄우지 않도록 동일 분기 적용.
|
||||
|
||||
`codexExecArgs`는 다음과 같이 단순화:
|
||||
|
||||
```go
|
||||
func codexExecArgs(profile config.CLIProfileConf, externalSessionID, prompt string) []string {
|
||||
if externalSessionID == "" {
|
||||
return append(append([]string{}, profile.Args...), prompt)
|
||||
}
|
||||
return append(append([]string{}, profile.ResumeArgs...), externalSessionID, prompt)
|
||||
}
|
||||
```
|
||||
|
||||
`codexResumeOptions`와 `isCodexExecProfile`, `filepath` import는 모두 삭제. `executeCodexExec`가 `profile.ResumeArgs == nil`인 경우 명시적 에러를 반환:
|
||||
|
||||
```go
|
||||
if profile.ResumeArgs == nil {
|
||||
return fmt.Errorf("cli adapter: codex-exec mode requires resume_args in profile %q", agent)
|
||||
}
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/codex_exec.go`에서 `isCodexExecProfile`, `codexResumeOptions`를 삭제한다.
|
||||
- [ ] `codexExecArgs` 시그니처를 새 형태로 바꾸고 호출부를 갱신한다.
|
||||
- [ ] `apps/node/internal/adapters/cli/cli.go`의 `Execute`/`Start`/`TerminateSession`에서 분기 조건을 `profile.Mode == "codex-exec"`로 통일한다.
|
||||
- [ ] `executeCodexExec` 진입부에 `ResumeArgs == nil` 가드를 추가한다.
|
||||
- [ ] 더 이상 사용하지 않는 `path/filepath` import를 제거한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`apps/node/internal/adapters/cli/codex_exec_blackbox_test.go`(`cli_test_layout_cleanup` 작업 후 위치) 또는 현재 `persistent/codex_exec_test.go`에 다음 케이스를 추가/수정:
|
||||
|
||||
- `TestCodexExecArgs_NoSessionUsesProfileArgs`
|
||||
- `TestCodexExecArgs_WithSessionUsesResumeArgs`
|
||||
- `TestExecuteCodexExec_MissingResumeArgsErrors`
|
||||
|
||||
기존 `codexResumeOptions` 단위 테스트는 모두 제거.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/cli/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 케이스 포함 PASS, 옵션 화이트리스트 관련 케이스가 모두 사라진 상태.
|
||||
|
||||
### [REFACTOR-3] edge.yaml codex 프로파일 갱신
|
||||
|
||||
#### 문제
|
||||
|
||||
`configs/edge.yaml:59-73`의 codex 프로파일은 `persistent: true`만 명시하며, mode/resume_args가 없다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
```yaml
|
||||
codex:
|
||||
command: "codex"
|
||||
args:
|
||||
- "exec"
|
||||
- "--dangerously-bypass-approvals-and-sandbox"
|
||||
- "--color"
|
||||
- "never"
|
||||
- "--skip-git-repo-check"
|
||||
- "--json"
|
||||
resume_args:
|
||||
- "exec"
|
||||
- "resume"
|
||||
- "--json"
|
||||
env: []
|
||||
persistent: false # mode가 분기를 결정하므로 false
|
||||
terminal: false
|
||||
output_format: "codex-json"
|
||||
mode: "codex-exec"
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `configs/edge.yaml`의 codex 프로파일에 `mode: codex-exec`와 `resume_args` 리스트를 추가한다.
|
||||
- [ ] `persistent`는 `false`로 설정해 일반 persistent 분기와 혼동되지 않게 한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
별도 단위 테스트는 추가하지 않는다. `[REFACTOR-1]`/`[REFACTOR-2]` 테스트와 최종 `go test ./...`로 회귀 검증.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go run ./apps/edge/cmd/edge --config configs/edge.yaml config print 2>/dev/null || true
|
||||
rg -n 'mode: "codex-exec"|resume_args:' configs/edge.yaml
|
||||
```
|
||||
|
||||
예상 결과: `mode`, `resume_args`가 codex 프로파일 아래에 보인다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `packages/config/config.go` | REFACTOR-1 |
|
||||
| `packages/config/config_test.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-2 |
|
||||
| `apps/node/internal/adapters/cli/codex_exec.go` | REFACTOR-2 |
|
||||
| `apps/node/internal/adapters/cli/codex_exec_blackbox_test.go` (또는 현 위치) | REFACTOR-2 |
|
||||
| `configs/edge.yaml` | REFACTOR-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
예상 결과: 전부 PASS, command sniffing 잔재 없음.
|
||||
74
agent-task/07_cli_profile_proto_message/CODE_REVIEW.md
Normal file
74
agent-task/07_cli_profile_proto_message/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!-- task=07_cli_profile_proto_message plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=07_cli_profile_proto_message, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log`
|
||||
2. `PLAN.md` → `plan_M.log`
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] proto 메시지 정의 및 생성물 갱신 | [ ] |
|
||||
| [API-2] edge buildConfigPayload typed 메시지 갱신 | [ ] |
|
||||
| [API-3] node BuildFromPayload typed 메시지 갱신 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `proto/iop/runtime.proto`에 `oneof config`가 추가되고 기존 `settings` 필드가 backward-compat을 위해 유지되었는지
|
||||
- `proto/gen/iop/runtime.pb.go`가 사람이 직접 편집되지 않고 `make proto`로 생성된 변경인지
|
||||
- edge `buildConfigPayload`가 cli/ollama/vllm 모두 oneof로 채우는지, 더이상 `stringsToAny` 등 우회 헬퍼가 남아있지 않은지
|
||||
- node `BuildFromPayload`에서 `cliConfFromStruct`/`boolFromAny`/`intFromAny`가 모두 제거되었는지
|
||||
- Persistent/Terminal/Idle timeout 정수 필드가 손실 없이 전달되는 단위 테스트가 추가되었는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### API-1 중간 검증
|
||||
```
|
||||
$ make proto
|
||||
$ go build ./proto/gen/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```
|
||||
$ go test ./apps/edge/internal/transport/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ make proto
|
||||
$ go build ./...
|
||||
$ go test ./...
|
||||
(output)
|
||||
```
|
||||
181
agent-task/07_cli_profile_proto_message/PLAN.md
Normal file
181
agent-task/07_cli_profile_proto_message/PLAN.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<!-- task=07_cli_profile_proto_message plan=0 tag=API -->
|
||||
|
||||
# CLI 프로파일 전용 proto 메시지 도입
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/07_cli_profile_proto_message/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
edge가 node에 보내는 `NodeConfigPayload`에서 CLI 프로파일은 `AdapterConfig.settings` (`google.protobuf.Struct`)로 직렬화되고, node 쪽에서 손으로 디코드합니다. (`apps/edge/internal/transport/server.go:190-207`의 `buildConfigPayload`와 `apps/node/internal/adapters/factory.go:69-118`의 `cliConfFromStruct`) 프로파일에 새 필드를 추가하면 edge·node 양쪽을 손으로 맞춰야 하며, structpb 인코딩 특성상 정수가 float로 들어가는 등의 변환 코드가 필요합니다. domain rule(`agent-ops/rules/project/rules.md`의 "protobuf 계약 변경은 .proto부터")을 따라 CLI 프로파일 전용 proto 메시지를 정의하고, edge·node 양쪽에서 구조체 단위로 주고받게 합니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. `[API-1]` `proto/iop/runtime.proto`에 `CLIAdapterConfig` / `CLIProfileConfig` 메시지를 추가하고 `make proto`로 생성물 갱신.
|
||||
2. `[API-2]` edge `buildConfigPayload`가 새 메시지로 직접 채우도록 변경.
|
||||
3. `[API-3]` node `BuildFromPayload`가 structpb 디코드 대신 새 메시지를 그대로 사용.
|
||||
|
||||
### [API-1] proto 메시지 정의 및 생성물 갱신
|
||||
|
||||
#### 문제
|
||||
|
||||
`proto/iop/runtime.proto:96-100`의 `AdapterConfig`는 `google.protobuf.Struct settings`로 모든 어댑터 타입의 설정을 우회 직렬화합니다. CLI 프로파일은 필드가 9개로 늘어나 string-keyed map 직렬화의 위험이 큽니다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`proto/iop/runtime.proto`의 `AdapterConfig`에 `oneof config`를 도입해 어댑터 타입별 typed 메시지를 가질 수 있도록 합니다. CLI 전용 메시지는 다음과 같이 정의합니다.
|
||||
|
||||
```proto
|
||||
message AdapterConfig {
|
||||
string type = 1;
|
||||
bool enabled = 2;
|
||||
google.protobuf.Struct settings = 3; // 구버전 호환용; 신규 어댑터는 oneof 사용 권장
|
||||
oneof config {
|
||||
CLIAdapterConfig cli = 4;
|
||||
OllamaAdapterConfig ollama = 5;
|
||||
VllmAdapterConfig vllm = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message CLIAdapterConfig {
|
||||
map<string, CLIProfileConfig> profiles = 1;
|
||||
}
|
||||
|
||||
message CLIProfileConfig {
|
||||
string command = 1;
|
||||
repeated string args = 2;
|
||||
repeated string env = 3;
|
||||
bool persistent = 4;
|
||||
bool terminal = 5;
|
||||
int32 response_idle_timeout_ms = 6;
|
||||
int32 startup_idle_timeout_ms = 7;
|
||||
string output_format = 8;
|
||||
}
|
||||
|
||||
message OllamaAdapterConfig { string base_url = 1; }
|
||||
message VllmAdapterConfig { string endpoint = 1; }
|
||||
```
|
||||
|
||||
오래된 `settings` 필드는 일단 유지해 mock/기타 어댑터 호환을 깨지 않습니다. CLI/Ollama/Vllm은 신규 oneof 경로만 사용하도록 합니다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `proto/iop/runtime.proto`에 `CLIAdapterConfig`, `CLIProfileConfig`, `OllamaAdapterConfig`, `VllmAdapterConfig` 메시지를 추가한다.
|
||||
- [ ] `AdapterConfig`에 `oneof config { ... }`를 추가한다 (`settings` 필드는 그대로 둔다).
|
||||
- [ ] `make proto`를 실행해 `proto/gen/iop/runtime.pb.go`를 갱신한다.
|
||||
- [ ] 생성 파일은 직접 수정하지 않는다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
테스트는 `[API-2]`/`[API-3]`에서 통합 검증한다. proto 정의 단독으로는 별도 단위 테스트를 작성하지 않는다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
make proto
|
||||
git diff proto/iop/runtime.proto
|
||||
go build ./proto/gen/...
|
||||
```
|
||||
|
||||
예상 결과: `runtime.proto`에 새 메시지/oneof가 추가되어 있고, 생성물이 빌드된다.
|
||||
|
||||
### [API-2] edge `buildConfigPayload`를 typed 메시지로 갱신
|
||||
|
||||
#### 문제
|
||||
|
||||
`apps/edge/internal/transport/server.go:155-209`의 `buildConfigPayload`가 `structpb.NewStruct(map[string]any{...})`로 CLI/Ollama/Vllm 설정을 우회 직렬화합니다. `stringsToAny` 같은 보조 함수도 필요합니다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
CLI/Ollama/Vllm은 `oneof config`로 직접 채우고, mock만 기존 `settings` 경로를 유지합니다.
|
||||
|
||||
```go
|
||||
if rec.Adapters.CLI.Enabled {
|
||||
profiles := make(map[string]*iop.CLIProfileConfig, len(rec.Adapters.CLI.Profiles))
|
||||
for name, p := range rec.Adapters.CLI.Profiles {
|
||||
profiles[name] = &iop.CLIProfileConfig{
|
||||
Command: p.Command,
|
||||
Args: append([]string(nil), p.Args...),
|
||||
Env: append([]string(nil), p.Env...),
|
||||
Persistent: p.Persistent,
|
||||
Terminal: p.Terminal,
|
||||
ResponseIdleTimeoutMs: int32(p.ResponseIdleTimeoutMS),
|
||||
StartupIdleTimeoutMs: int32(p.StartupIdleTimeoutMS),
|
||||
OutputFormat: p.OutputFormat,
|
||||
}
|
||||
}
|
||||
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
|
||||
Type: "cli", Enabled: true,
|
||||
Config: &iop.AdapterConfig_Cli{Cli: &iop.CLIAdapterConfig{Profiles: profiles}},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/edge/internal/transport/server.go`의 `buildConfigPayload`에서 cli/ollama/vllm 분기를 oneof 기반으로 다시 쓴다.
|
||||
- [ ] `stringsToAny` 헬퍼는 더 이상 사용되지 않으면 제거한다.
|
||||
- [ ] mock 어댑터 분기는 그대로 둔다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`apps/edge/internal/transport/server_test.go`에 `TestBuildConfigPayload_CLIOneof` (가칭)를 추가해 CLI 프로파일 한 개 정의에서 oneof 경로가 채워지고, settings 필드는 비어 있는지 단언한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/edge/internal/transport/...
|
||||
```
|
||||
|
||||
예상 결과: 신규 테스트 포함 PASS.
|
||||
|
||||
### [API-3] node `BuildFromPayload`를 typed 메시지 기반으로 갱신
|
||||
|
||||
#### 문제
|
||||
|
||||
`apps/node/internal/adapters/factory.go:69-118`의 `cliConfFromStruct`가 structpb를 손으로 디코드하면서 `boolFromAny`, `intFromAny` 같은 헬퍼에 의존합니다. structpb의 number→float64 변환 등 함정이 많습니다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`AdapterConfig.GetCli()`가 nil이 아니면 typed 메시지 그대로 `config.CLIConf`로 매핑하고, structpb 경로(`cliConfFromStruct`)는 제거합니다. Ollama/Vllm도 동일하게 변경.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/node/internal/adapters/factory.go`의 `cli/ollama/vllm` 분기를 oneof 기반으로 재작성한다.
|
||||
- [ ] `cliConfFromStruct`, `ollamaConfFromStruct`, `vllmConfFromStruct`, `boolFromAny`, `intFromAny`를 모두 제거한다.
|
||||
- [ ] `apps/node/internal/adapters/factory_internal_test.go`와 `factory_test.go`의 케이스를 typed 메시지 입력 기반으로 갱신한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`factory_test.go`의 기존 시나리오(여러 어댑터 enabled, CLI 프로파일 매핑)를 oneof 입력으로 다시 작성한다. 신규 케이스: `Persistent`, `ResponseIdleTimeoutMS`, `StartupIdleTimeoutMS`가 정확히 0이 아닌 값으로 전달되는지 단언.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/node/internal/adapters/...
|
||||
```
|
||||
|
||||
예상 결과: PASS.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `proto/iop/runtime.proto` | API-1 |
|
||||
| `proto/gen/iop/runtime.pb.go` (생성물) | API-1 |
|
||||
| `apps/edge/internal/transport/server.go` | API-2 |
|
||||
| `apps/edge/internal/transport/server_test.go` | API-2 |
|
||||
| `apps/node/internal/adapters/factory.go` | API-3 |
|
||||
| `apps/node/internal/adapters/factory_internal_test.go` | API-3 |
|
||||
| `apps/node/internal/adapters/factory_test.go` | API-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
make proto
|
||||
go build ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
예상 결과: build/test 모두 PASS, 새 oneof 경로로 edge↔node가 동작.
|
||||
|
|
@ -13,7 +13,7 @@ metrics:
|
|||
|
||||
console:
|
||||
adapter: "cli"
|
||||
agent: "opencode"
|
||||
agent: "cline-m1"
|
||||
session_id: "default"
|
||||
background: false
|
||||
timeout_sec: 300
|
||||
|
|
|
|||
Loading…
Reference in a new issue