diff --git a/agent-task/01_cli_capabilities_real_profiles/CODE_REVIEW.md b/agent-task/01_cli_capabilities_real_profiles/CODE_REVIEW.md new file mode 100644 index 0000000..8aa9860 --- /dev/null +++ b/agent-task/01_cli_capabilities_real_profiles/CODE_REVIEW.md @@ -0,0 +1,58 @@ + + +# 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) +``` diff --git a/agent-task/01_cli_capabilities_real_profiles/PLAN.md b/agent-task/01_cli_capabilities_real_profiles/PLAN.md new file mode 100644 index 0000000..a1a1c9e --- /dev/null +++ b/agent-task/01_cli_capabilities_real_profiles/PLAN.md @@ -0,0 +1,84 @@ + + +# 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`는 매치 없음. diff --git a/agent-task/02_cli_test_layout_cleanup/CODE_REVIEW.md b/agent-task/02_cli_test_layout_cleanup/CODE_REVIEW.md new file mode 100644 index 0000000..05bdc6d --- /dev/null +++ b/agent-task/02_cli_test_layout_cleanup/CODE_REVIEW.md @@ -0,0 +1,59 @@ + + +# 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) +``` diff --git a/agent-task/02_cli_test_layout_cleanup/PLAN.md b/agent-task/02_cli_test_layout_cleanup/PLAN.md new file mode 100644 index 0000000..46a9772 --- /dev/null +++ b/agent-task/02_cli_test_layout_cleanup/PLAN.md @@ -0,0 +1,79 @@ + + +# 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`만 보인다. diff --git a/agent-task/03_cli_emitter_interface/CODE_REVIEW.md b/agent-task/03_cli_emitter_interface/CODE_REVIEW.md new file mode 100644 index 0000000..747a0a8 --- /dev/null +++ b/agent-task/03_cli_emitter_interface/CODE_REVIEW.md @@ -0,0 +1,74 @@ + + +# 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) +``` diff --git a/agent-task/03_cli_emitter_interface/PLAN.md b/agent-task/03_cli_emitter_interface/PLAN.md new file mode 100644 index 0000000..ebd02b6 --- /dev/null +++ b/agent-task/03_cli_emitter_interface/PLAN.md @@ -0,0 +1,233 @@ + + +# 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 보일러플레이트가 한 곳으로 모인다. diff --git a/agent-task/04_cli_persistent_cancel_reason/CODE_REVIEW.md b/agent-task/04_cli_persistent_cancel_reason/CODE_REVIEW.md new file mode 100644 index 0000000..df564a0 --- /dev/null +++ b/agent-task/04_cli_persistent_cancel_reason/CODE_REVIEW.md @@ -0,0 +1,58 @@ + + +# 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) +``` diff --git a/agent-task/04_cli_persistent_cancel_reason/PLAN.md b/agent-task/04_cli_persistent_cancel_reason/PLAN.md new file mode 100644 index 0000000..abf6198 --- /dev/null +++ b/agent-task/04_cli_persistent_cancel_reason/PLAN.md @@ -0,0 +1,107 @@ + + +# 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됨. diff --git a/agent-task/05_cli_persistent_explicit_complete/CODE_REVIEW.md b/agent-task/05_cli_persistent_explicit_complete/CODE_REVIEW.md new file mode 100644 index 0000000..5ab4b7e --- /dev/null +++ b/agent-task/05_cli_persistent_explicit_complete/CODE_REVIEW.md @@ -0,0 +1,72 @@ + + +# 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) +``` diff --git a/agent-task/05_cli_persistent_explicit_complete/PLAN.md b/agent-task/05_cli_persistent_explicit_complete/PLAN.md new file mode 100644 index 0000000..3da4421 --- /dev/null +++ b/agent-task/05_cli_persistent_explicit_complete/PLAN.md @@ -0,0 +1,218 @@ + + +# 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: "<>" +``` + +#### 수정 파일 및 체크리스트 + +- [ ] `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가 `"<>"` 라인을 보내면 즉시 `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으로 구분 가능. diff --git a/agent-task/06_cli_codex_explicit_mode/CODE_REVIEW.md b/agent-task/06_cli_codex_explicit_mode/CODE_REVIEW.md new file mode 100644 index 0000000..c0070e0 --- /dev/null +++ b/agent-task/06_cli_codex_explicit_mode/CODE_REVIEW.md @@ -0,0 +1,72 @@ + + +# 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) +``` diff --git a/agent-task/06_cli_codex_explicit_mode/PLAN.md b/agent-task/06_cli_codex_explicit_mode/PLAN.md new file mode 100644 index 0000000..a48877e --- /dev/null +++ b/agent-task/06_cli_codex_explicit_mode/PLAN.md @@ -0,0 +1,200 @@ + + +# 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 잔재 없음. diff --git a/agent-task/07_cli_profile_proto_message/CODE_REVIEW.md b/agent-task/07_cli_profile_proto_message/CODE_REVIEW.md new file mode 100644 index 0000000..6e6e730 --- /dev/null +++ b/agent-task/07_cli_profile_proto_message/CODE_REVIEW.md @@ -0,0 +1,74 @@ + + +# 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) +``` diff --git a/agent-task/07_cli_profile_proto_message/PLAN.md b/agent-task/07_cli_profile_proto_message/PLAN.md new file mode 100644 index 0000000..895ab79 --- /dev/null +++ b/agent-task/07_cli_profile_proto_message/PLAN.md @@ -0,0 +1,181 @@ + + +# 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 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가 동작. diff --git a/configs/edge.yaml b/configs/edge.yaml index 44e13fe..eba4fcf 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -13,7 +13,7 @@ metrics: console: adapter: "cli" - agent: "opencode" + agent: "cline-m1" session_id: "default" background: false timeout_sec: 300