<!-- task=m-architecture-refactor-foundation/04_cli_executor_split plan=0 tag=REFACTOR -->

# Plan - REFACTOR CLI Executor Split

## 이 파일을 읽는 구현 에이전트에게

구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, 실제 stdout/stderr를 기록하고, active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 선택, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌이 있으면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 기록하고 멈춘다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 재실행이나 산출물 수집으로 닫을 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 `cli` adapter는 mode dispatch, profile/session map, persistent lifecycle, provider별 one-shot 실행이 `CLI` 구조체와 여러 파일에 얽혀 있다. 이 작업은 실행 전략과 session ownership을 분리해 `terminal-core` 후속 분리가 들어갈 수 있는 안정적인 경계를 만든다. 외부 adapter API와 wire 계약은 유지한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 양식을 복사한 것이다. 구현 중 direct user prompt는 금지되며, code-review가 요청 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md`
- Task ids:
  - `cli-executor-split`: apps/node/internal/adapters/cli mode dispatch/session map ownership executor/strategy split
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-test/local/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/node-smoke.md`
- `apps/node/internal/adapters/cli/cli.go`
- `apps/node/internal/adapters/cli/oneshot.go`
- `apps/node/internal/adapters/cli/persistent.go`
- `apps/node/internal/adapters/cli/codex_exec.go`
- `apps/node/internal/adapters/cli/antigravity_print.go`
- `apps/node/internal/adapters/cli/opencode_sse.go`
- `apps/node/internal/adapters/cli/persistent_output_filter.go`
- `apps/node/internal/adapters/cli/cli_internal_test.go`
- `apps/node/internal/adapters/cli/oneshot_blackbox_test.go`
- `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go`
- `apps/node/internal/adapters/cli/codex_exec_blackbox_test.go`
- `apps/node/internal/adapters/cli/antigravity_print_blackbox_test.go`
- `apps/node/internal/adapters/cli/opencode_sse_internal_test.go`
- `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`
- `apps/node/internal/adapters/cli/lifecycle_blackbox_test.go`
- `apps/node/internal/adapters/cli/persistent_output_filter_test.go`
- `apps/node/internal/adapters/cli/status/status_test.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/bootstrap/module.go`
- `apps/node/internal/bootstrap/module_test.go`
- `scripts/e2e-smoke.sh`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었고, Node 변경은 `node-smoke` profile을 매칭한다.
- `agent-test/local/node-smoke.md`를 읽었다. 적용 명령은 `go test -count=1 ./apps/node/...`, `./scripts/e2e-smoke.sh`이다.
- Node domain rule상 사용자 실행 경로 변경은 local node test와 smoke/full-cycle 확인이 필요하다. 이 계획의 최종 검증은 재현 가능한 local smoke까지 고정하고, `scripts/dev/edge.sh` + `scripts/dev/node.sh` 수동 full-cycle 미실행은 review stub에 리스크로 기록한다.

### 테스트 커버리지 공백

- mode dispatch 추출: 기존 CLI blackbox tests가 one-shot, persistent, codex exec, antigravity, opencode SSE 실행 경로를 덮지만, 새 executor registry/resolver 단위 테스트가 필요하다.
- session map ownership 이동: 기존 session list/terminate/lifecycle tests가 외부 동작을 덮지만, map ownership이 executor 쪽으로 이동한 뒤 mode별 session 수집/종료 단위 테스트가 필요하다.
- public adapter API 유지: `apps/node/internal/node`와 bootstrap tests가 간접 커버한다.

### 심볼 참조

- 계획 단계에서 제거/rename 확정 symbol은 없음.
- 구현 중 `CLI.Execute`, `CLI.Start`, `CLI.Stop`, `CLI.TerminateSession`, `handleSessionList`, `resolveSession`, `startProfileSession`, `executeOpencodeSSE`, `executeOneShot`의 call site를 `rg --sort path`로 재확인한다.

### 분할 판단

- split decision policy를 계획 파일 선택 전에 평가했다.
- shared task group: `agent-task/m-architecture-refactor-foundation/`.
- `04_cli_executor_split`: mode dispatch와 session ownership refactor. predecessor 없음.
- `05+04_terminal_core`: PTY/screen/input/signal/close core 분리. predecessor `04`가 PASS 후 `complete.log`를 생산해야 구현 가능하다.
- `04`는 terminal core보다 먼저 완료되어야 한다. `CLI`의 session ownership이 분리되어야 terminal session core가 좁은 API로 들어간다.

### 범위 결정 근거

- 이 계획은 `apps/node/internal/adapters/cli` 내부 refactor로 제한한다.
- `apps/node/internal/adapters/cli/persistent.go`의 PTY screen rendering, resize/input/signal/close core 분리는 `05+04_terminal_core`로 제외한다.
- router default mock policy와 vLLM disabled policy는 이미 직접 처리된 작은 작업 범위이며 이 계획에 포함하지 않는다.
- Edge console command surface, proto schema, config schema 변경은 외부 계약 변경이므로 제외한다.

### 빌드 등급

- `cloud-G07`: shell/CLI workflow, process control, stdout/stderr parsing, session lifecycle이 중심이고 mode별 회귀 위험이 높다.

## 구현 체크리스트

- [ ] mode dispatch를 executor/strategy 경계로 추출하고 `CLI.Execute`의 mode switch를 단일 resolver 호출로 축소한다.
- [ ] persistent/codex/antigravity/opencode session map ownership을 executor 또는 session store 경계로 이동하고 session list/terminate/start/stop 동작을 유지한다.
- [ ] mode resolver와 session ownership 이동을 검증하는 focused tests를 추가하거나 기존 tests를 갱신한다.
- [ ] `go test -count=1 ./apps/node/internal/adapters/cli` 중간 검증을 통과시킨다.
- [ ] `go test -count=1 ./apps/node/...`와 `./scripts/e2e-smoke.sh` 최종 검증을 통과시키고 수동 full-cycle 미실행 여부를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Mode Dispatch Extract

#### 문제

`apps/node/internal/adapters/cli/cli.go:206`-`225`에서 `CLI.Execute`가 mode 문자열과 provider별 실행 함수를 직접 알고 있다.

```go
// apps/node/internal/adapters/cli/cli.go:206
func (c *CLI) Execute(ctx context.Context, req runtime.Request, emit runtime.Emitter) error {
	cfg, err := c.resolveTarget(req.Target)
	if err != nil {
		return err
	}

	mode := cfg.Mode
	switch {
	case mode == modeCodexExec:
		return c.executeCodexExec(ctx, cfg, req, emit)
	case mode == modeAntigravity:
		return c.executeAntigravity(ctx, cfg, req, emit)
	case mode == modeOpencodeSSE:
		return c.executeOpencodeSSE(ctx, cfg, req, emit)
	case mode == "" || mode == modePersistentLazy:
		return c.executePersistent(ctx, cfg, req, emit)
	default:
		return c.executeOneShot(ctx, cfg, req, emit)
	}
}
```

#### 해결 방법

새 내부 executor interface를 추가한다. 예시는 `apps/node/internal/adapters/cli/executor.go`를 권장하지만, 기존 파일 수정을 우선하는 원칙에 맞게 파일 수 증가가 과하면 `cli.go` 하단에 둘 수 있다.

```go
// apps/node/internal/adapters/cli/executor.go
type executor interface {
	Execute(context.Context, Config, runtime.Request, runtime.Emitter) error
}

func (c *CLI) executorFor(cfg Config) executor {
	switch cfg.Mode {
	case modeCodexExec:
		return c.codexExecutor
	case modeAntigravity:
		return c.antigravityExecutor
	case modeOpencodeSSE:
		return c.opencodeExecutor
	case "", modePersistentLazy:
		return c.persistentExecutor
	default:
		return c.oneShotExecutor
	}
}
```

`CLI.Execute`는 target resolve와 executor 호출만 남긴다.

```go
func (c *CLI) Execute(ctx context.Context, req runtime.Request, emit runtime.Emitter) error {
	cfg, err := c.resolveTarget(req.Target)
	if err != nil {
		return err
	}
	return c.executorFor(cfg).Execute(ctx, cfg, req, emit)
}
```

#### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/cli/cli.go`: `CLI.Execute` mode switch 제거, resolver 호출로 축소.
- [ ] `apps/node/internal/adapters/cli/oneshot.go`: one-shot 실행 함수를 executor method로 감싸거나 adapter method를 보존한 wrapper 추가.
- [ ] `apps/node/internal/adapters/cli/codex_exec.go`: codex exec executor 추가.
- [ ] `apps/node/internal/adapters/cli/antigravity_print.go`: antigravity executor 추가.
- [ ] `apps/node/internal/adapters/cli/opencode_sse.go`: opencode SSE executor 추가.

#### 테스트 작성

- `apps/node/internal/adapters/cli/cli_internal_test.go`에 `TestExecutorForMode`를 추가한다.
- 검증 목표: `codex_exec`, `antigravity`, `opencode_sse`, `persistent_lazy`, empty mode, unknown mode가 기존 실행 경로와 같은 executor로 resolve된다.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters/cli
```

### [REFACTOR-2] Session Map Ownership Split

#### 문제

`apps/node/internal/adapters/cli/cli.go:86`-`97`에서 `CLI`가 모든 mode의 session map을 직접 소유한다. `Start`, `Stop`, `handleSessionList`, `TerminateSession`도 각 map을 직접 순회한다.

```go
// apps/node/internal/adapters/cli/cli.go:86
type CLI struct {
	mu                  sync.Mutex
	targets             map[string]Config
	defaultTarget       string
	sessions            map[sessionKey]*profileSession
	codexExecSessions   map[sessionKey]*codexExecSession
	antigravitySessions map[sessionKey]*antigravitySession
	opencodeSessions    map[sessionKey]*opencodeSSESession
	idle                time.Duration
	logger              *zap.Logger
}
```

`apps/node/internal/adapters/cli/cli.go:299`-`338`의 session list와 `cli.go:341`-`391`의 terminate 경로가 map 구조에 결합되어 있다.

#### 해결 방법

mode별 executor가 자기 session store를 소유하게 하고, `CLI`는 공통 lifecycle 호출과 session snapshot/terminate 인터페이스만 사용한다. 외부 `CLI` public method signature는 유지한다.

```go
type sessionReporter interface {
	Sessions() []SessionInfo
	Terminate(sessionID string) (bool, error)
	Stop(context.Context) error
}

type persistentExecutor struct {
	mu       *sync.Mutex
	sessions map[sessionKey]*profileSession
}
```

`handleSessionList`는 executor들의 `Sessions()` 결과를 합쳐 기존 `SessionInfo` shape을 유지한다. `TerminateSession`은 executor들을 순서대로 호출하고 기존 에러 메시지 의미를 유지한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/cli/cli.go`: session maps를 `CLI`에서 executor/session store로 이동.
- [ ] `apps/node/internal/adapters/cli/persistent.go`: persistent session store를 `persistentExecutor` 쪽으로 이동.
- [ ] `apps/node/internal/adapters/cli/codex_exec.go`: codex exec session store ownership 이동.
- [ ] `apps/node/internal/adapters/cli/antigravity_print.go`: antigravity session store ownership 이동.
- [ ] `apps/node/internal/adapters/cli/opencode_sse.go`: opencode SSE session store ownership 이동.
- [ ] `apps/node/internal/adapters/cli/lifecycle_blackbox_test.go`: `Stop`/terminate/list behavior가 유지되는지 갱신.

#### 테스트 작성

- `apps/node/internal/adapters/cli/lifecycle_blackbox_test.go`에 session list/terminate가 executor-owned stores를 모두 포함하는 regression test를 추가한다.
- 기존 persistent/opencode/codex/antigravity tests는 통과시킨다.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters/cli
```

### [REFACTOR-3] Verification And Contract Guard

#### 문제

refactor 이후 외부 contract는 변하지 않아야 한다. `apps/node/internal/node/node.go`와 bootstrap 경로는 adapter registry를 통해 `CLI.Execute`, `OnCommand`, `Cancel`을 호출하므로 signature drift가 있으면 integration이 깨진다.

#### 해결 방법

새 interface가 내부에만 머물도록 한다. public `CLI` methods와 config field names를 유지하고, tests는 CLI package와 node package 양쪽에서 확인한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: resolver/session ownership focused tests 추가.
- [ ] `apps/node/internal/adapters/cli/*_blackbox_test.go`: refactor에 맞춘 setup 갱신만 수행.
- [ ] `apps/node/internal/node/node_test.go`: 변경 필요 시 public behavior 유지 확인.
- [ ] `apps/node/internal/bootstrap/module_test.go`: 변경 필요 시 adapter construction 유지 확인.

#### 테스트 작성

- 작성함. REFACTOR-1/2 tests가 refactor 회귀를 직접 잡는다.
- 별도 public API test가 깨지지 않으면 node/bootstrap tests는 기존 테스트 재사용으로 충분하다.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/node ./apps/node/internal/bootstrap
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-1, REFACTOR-2 |
| `apps/node/internal/adapters/cli/oneshot.go` | REFACTOR-1 |
| `apps/node/internal/adapters/cli/persistent.go` | REFACTOR-1, REFACTOR-2 |
| `apps/node/internal/adapters/cli/codex_exec.go` | REFACTOR-1, REFACTOR-2 |
| `apps/node/internal/adapters/cli/antigravity_print.go` | REFACTOR-1, REFACTOR-2 |
| `apps/node/internal/adapters/cli/opencode_sse.go` | REFACTOR-1, REFACTOR-2 |
| `apps/node/internal/adapters/cli/cli_internal_test.go` | REFACTOR-1, REFACTOR-3 |
| `apps/node/internal/adapters/cli/lifecycle_blackbox_test.go` | REFACTOR-2 |
| `apps/node/internal/adapters/cli/*_blackbox_test.go` | REFACTOR-3 |

## 최종 검증

```bash
go test -count=1 ./apps/node/internal/adapters/cli
go test -count=1 ./apps/node/...
./scripts/e2e-smoke.sh
git diff --check
```

예상 결과: 모든 명령 exit code 0. `./scripts/e2e-smoke.sh`의 안내 문구에 따라 수동 `scripts/dev/edge.sh` + `scripts/dev/node.sh` full-cycle을 실행하지 못한 경우 review stub에 미실행 리스크를 기록한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
