200 lines
8.9 KiB
Markdown
200 lines
8.9 KiB
Markdown
<!-- 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 잔재 없음.
|