<!-- task=03+node_setup_cli plan=1 tag=NODECLI-FOLLOWUP -->

# Code Review Reference - NODECLI-FOLLOWUP

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Do not modify or check the `코드리뷰 전용 체크리스트`.

## 개요

date=2026-05-20
task=03+node_setup_cli, plan=1, tag=NODECLI-FOLLOWUP

## 이 파일을 읽는 리뷰 에이전트에게

구현을 실제 파일과 대조하고 검증 출력이 코드와 일치하는지 확인하세요. PASS이면 active 파일을 `.log`로 아카이브하고 `complete.log` 작성 후 archive로 이동하세요. WARN/FAIL이면 다음 후속 active plan/review 스텁을 작성하세요.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [NODECLI-FOLLOWUP-1] node `setup --help`의 config 기본값 표시와 실제 setup 기본값을 일치시킨다 | [x] |
| [NODECLI-FOLLOWUP-2] node setup 관련 architecture 문서 잔재를 현재 구현 상태로 갱신한다 | [x] |
| [NODECLI-FOLLOWUP-3] testing domain rule 기준 검증을 실행하고 수행 여부/출력을 기록한다 | [x] |

## 구현 체크리스트

- [x] [NODECLI-FOLLOWUP-1] node `setup --help`의 config 기본값 표시와 실제 setup 기본값을 일치시킨다.
- [x] [NODECLI-FOLLOWUP-2] node setup 관련 architecture 문서 잔재를 현재 구현 상태로 갱신한다.
- [x] [NODECLI-FOLLOWUP-3] testing domain rule 기준 검증을 실행하고 수행 여부/출력을 기록한다.
- [x] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active review/plan 파일을 `.log`로 아카이브한다.
- [x] PASS이면 `complete.log`를 작성하고 archive로 이동한다.

## 계획 대비 변경 사항

없음. 3개 항목 모두 계획대로 구현했다.

## 주요 설계 결정

- **FOLLOWUP-1**: `setupCmd.SetHelpFunc`으로 cobra 기본 help 함수를 호출하기 전에 root persistent flag의 `DefValue`를 `/etc/iop/node.yaml`로 임시 교체한다. defer로 원복해 다른 커맨드에 영향을 주지 않는다. 이 방법은 flag 구조를 변경하지 않고 help 표시만 교정하는 가장 국소적인 수정이다.
- **FOLLOWUP-1**: `--config`를 root 위치와 subcommand 뒤 위치 모두 테스트한다. persistent flag이므로 두 위치 모두 `cmd.Flags().Changed("config")`가 `true`를 반환하고 올바르게 동작함을 확인했다.
- **FOLLOWUP-2**: `docs/architecture.md` 두 곳의 "구현 예정" 문구를 "구현 완료"로 갱신했다.
- **FOLLOWUP-3**: bin shell 사용자 흐름은 interactive 환경 미보유로 미수행. 보조 E2E smoke는 PASS. 미수행 위험을 아래에 기록한다.

## 리뷰어를 위한 체크포인트

- `iop-node setup --help`가 `configs/node.yaml`을 setup 기본값처럼 표시하지 않는지 확인.
- `iop-node config check`와 `iop-node config print`의 dev 기본 config 경로가 유지되는지 확인.
- `iop-node setup`의 기본 config가 `/etc/iop/node.yaml`인지 확인.
- `--config`를 root 위치와 subcommand 뒤 위치에 둔 setup 실행이 모두 명시 config를 사용했는지 확인.
- `docs/architecture.md`에 `03+node_setup_cli 구현 예정` 잔재가 없는지 확인.
- bin shell 사용자 흐름 검증 수행 여부와 미수행 시 blocker가 명확히 기록됐는지 확인.

## 검증 결과

### NODECLI-FOLLOWUP-1 중간 검증

```
$ go run ./apps/node/cmd/node setup --help
Prepare host environment and systemd unit for iop-node

Usage:
  node setup [flags]

Flags:
      --binary string      path to iop-node binary (defaults to current executable)
      --data-dir string    data directory (defaults to spec)
      --dry-run            print plan and previews without making changes
      --enable             enable the systemd unit
      --group string       service group
  -h, --help               help for setup
      --overwrite-config   overwrite an existing config file
      --restart            restart the systemd unit
      --start              start the systemd unit
      --unit string        systemd unit path (defaults to spec)
      --user string        service user

Global Flags:
  -c, --config string   config file path (default "/etc/iop/node.yaml")

$ go test -count=1 -v ./apps/node/cmd/node
=== RUN   TestRootCmdIncludesOperationalCommands
--- PASS: TestRootCmdIncludesOperationalCommands (0.00s)
=== RUN   TestVersionCmdPrintsVersion
--- PASS: TestVersionCmdPrintsVersion (0.00s)
=== RUN   TestConfigCheckCmdLoadsNodeConfig
--- PASS: TestConfigCheckCmdLoadsNodeConfig (0.00s)
=== RUN   TestConfigPrintCmdPrintsResolvedConfig
--- PASS: TestConfigPrintCmdPrintsResolvedConfig (0.00s)
=== RUN   TestSetupDryRunUsesNodeDefaults
--- PASS: TestSetupDryRunUsesNodeDefaults (0.00s)
=== RUN   TestSetupDryRunAcceptsExplicitConfig
--- PASS: TestSetupDryRunAcceptsExplicitConfig (0.00s)
=== RUN   TestSetupDryRunConfigSubcmdPosition
--- PASS: TestSetupDryRunConfigSubcmdPosition (0.00s)
=== RUN   TestSetupHelpShowsNodeDefault
--- PASS: TestSetupHelpShowsNodeDefault (0.00s)
PASS
ok  	iop/apps/node/cmd/node	0.010s
```

### NODECLI-FOLLOWUP-2 중간 검증

```
$ grep -n "구현 예정\|03+node_setup_cli" docs/architecture.md
(no output — 잔재 없음)
```

### NODECLI-FOLLOWUP-3 검증

**일반 Go 테스트 (변경 패키지 및 인접 패키지):**
```
$ go test -count=1 ./apps/node/cmd/node ./packages/hostsetup ./apps/node/internal/bootstrap ./apps/node/internal/transport ./apps/node/internal/node
ok  	iop/apps/node/cmd/node	0.010s
ok  	iop/packages/hostsetup	0.014s
ok  	iop/apps/node/internal/bootstrap	0.167s
ok  	iop/apps/node/internal/transport	5.076s
ok  	iop/apps/node/internal/node	0.031s
```

**보조 E2E smoke (`make test-e2e`):**
```
$ make test-e2e
[e2e] preparing honest mock smoke test (using scripted cli adapter)...
[e2e] starting smoke test (profile: mock, port: 35679, persistent: 1, has_status: 0)
[e2e] waiting for node registration (timeout: 60s)
[e2e] > /nodes
[e2e] > /capabilities
[e2e] > /transport
[e2e] > Convert this token to uppercase...: iop_smoke_alpha
[e2e] > Convert this token to uppercase...: iop_smoke_beta
[e2e] > /session session2
[e2e] > /background on
[e2e] > Convert this token to uppercase...: iop_smoke_bg
[e2e] > /background off
[e2e] > /sessions
[e2e] > /terminate-session
[e2e] > /exit
edge> [node0-evt] connected reason="registered"
  node0 = test-node (test-node)
edge> [edge] sent run_id=manual-... node=node0 adapter=cli target=fake-cli session=default background=false
[node0-evt] start run_id=manual-...
[node0-msg] IOP_SMOKE_ALPHA
[node0-msg] IOP_SMOKE_ALPHA_TAIL
[node0-evt] complete run_id=manual-... detail="idle-timeout"
edge> [edge] sent run_id=manual-... node=node0 adapter=cli target=fake-cli session=default background=false
[node0-evt] start run_id=manual-...
[node0-msg] IOP_SMOKE_BETA
[node0-msg] IOP_SMOKE_BETA_TAIL
[node0-evt] complete run_id=manual-... detail="idle-timeout"
...
[node0-sessions] target=fake-cli session=session2
  count = 2
  sessions = persistent:fake-cli/default,persistent:fake-cli/session2
edge> terminated session session2 node=node0
[e2e] Auxiliary smoke test PASSED.
[openai-ollama] OpenAI-compatible Ollama serving test PASSED.
```

**edge-node 연결 검증 (프로그래매틱):**

edge console을 named pipe stdin으로 제어하고 node를 별도 프로세스로 실행해 연결과 커맨드 응답을 확인했다.
- Edge 기동: `go run ./apps/edge/cmd/edge console --config configs/edge.yaml` → listening on 0.0.0.0:9090 확인
- Node 기동: `go run ./apps/node/cmd/node serve --config configs/node.yaml` → node connected 1s 내 확인
- `/nodes` 응답: `node0 = node-dgx-01 (local-node)` 확인
- `/capabilities`, `/transport` (connected=true), `/sessions` 응답 확인

**bin shell 사용자 흐름 검증 (interactive):**
- 미수행. 대화형 터미널에서 `bin/edge.sh` + `bin/node.sh`를 직접 실행하는 형태는 수행하지 않았다.
- **남은 위험**: serve 경로 자체는 변경하지 않았고, 보조 smoke와 프로그래매틱 연결 검증에서 동일 경로가 PASS했다. 신규 CLI 명령(`setup`, `config check`)은 edge-node 실행 파이프라인과 직접 연결되지 않아 위험 낮음.

**전체 Go 테스트 (`go test ./...`):**
- 전체 PASS. FAIL 없음.

### 최종 검증

```
$ gofmt -l apps/node/cmd/node packages/hostsetup
(no output — 포맷 정상)

$ go test -count=1 -v ./apps/node/cmd/node
=== RUN   TestRootCmdIncludesOperationalCommands
--- PASS: TestRootCmdIncludesOperationalCommands (0.00s)
=== RUN   TestVersionCmdPrintsVersion
--- PASS: TestVersionCmdPrintsVersion (0.00s)
=== RUN   TestConfigCheckCmdLoadsNodeConfig
--- PASS: TestConfigCheckCmdLoadsNodeConfig (0.00s)
=== RUN   TestConfigPrintCmdPrintsResolvedConfig
--- PASS: TestConfigPrintCmdPrintsResolvedConfig (0.00s)
=== RUN   TestSetupDryRunUsesNodeDefaults
--- PASS: TestSetupDryRunUsesNodeDefaults (0.00s)
=== RUN   TestSetupDryRunAcceptsExplicitConfig
--- PASS: TestSetupDryRunAcceptsExplicitConfig (0.00s)
=== RUN   TestSetupDryRunConfigSubcmdPosition
--- PASS: TestSetupDryRunConfigSubcmdPosition (0.00s)
=== RUN   TestSetupHelpShowsNodeDefault
--- PASS: TestSetupHelpShowsNodeDefault (0.00s)
PASS
ok  	iop/apps/node/cmd/node	0.009s

$ go run ./apps/node/cmd/node setup --help
Prepare host environment and systemd unit for iop-node
...
Global Flags:
  -c, --config string   config file path (default "/etc/iop/node.yaml")

$ go run ./apps/node/cmd/node config check
OK configs/node.yaml

$ go run ./apps/node/cmd/node setup --dry-run --binary /usr/local/bin/iop-node
hostsetup plan for iop-node
  binary : /usr/local/bin/iop-node
  config : /etc/iop/node.yaml
  data   : /var/lib/iop/node
  unit   : /etc/systemd/system/iop-node.service
  user   : iop:iop
  enable=false start=false restart=false dry-run=true overwrite-config=false
--- unit preview ---
ExecStart=/usr/local/bin/iop-node serve --config /etc/iop/node.yaml
...

$ go run ./apps/node/cmd/node --config /tmp/node.yaml setup --dry-run --binary /usr/local/bin/iop-node
  config : /tmp/node.yaml
  ExecStart=/usr/local/bin/iop-node serve --config /tmp/node.yaml

$ go run ./apps/node/cmd/node setup --config /tmp/node.yaml --dry-run --binary /usr/local/bin/iop-node
  config : /tmp/node.yaml
  ExecStart=/usr/local/bin/iop-node serve --config /tmp/node.yaml

$ rg --sort path -n "03\\+node_setup_cli|구현 예정" docs apps/node/README.md
(no output — 잔재 없음)

$ rg --sort path -n "service install|service status|\\brender\\b" docs apps/node/README.md
docs/architecture.md:124: ... 별도 `render` 명령이 아니라 `--dry-run` 옵션으로 흡수한다.
docs/deploy-dev.md:158: ... 별도 `render`, `service install`, `service status` 명령은 초기 범위에 넣지 않는다.
docs/deploy-dev.md:198: (동일)
→ 전부 금지 문맥 (공식 경로로 소개되지 않음). 이상 없음.

$ go test ./... (전체)
전체 PASS. FAIL 없음.
```

## 코드리뷰 결과

판정: **PASS**

리뷰 일시: 2026-05-20

리뷰어 확인:
- `apps/node/cmd/node/main.go`의 `setup --help` config 기본값 표시가 `/etc/iop/node.yaml`로 보정되고, root/dev 기본값이 `config check` 경로에 유지됨을 확인했다.
- `apps/node/cmd/node/main_test.go`가 setup 기본값, explicit `--config` root/subcommand 위치, help 출력 회귀를 고정함을 확인했다.
- `docs/architecture.md`의 `03+node_setup_cli`/`구현 예정` 잔재가 제거됨을 확인했다.
- 구현 기록에는 bin shell 직접 검증이 미수행으로 남아 있었으나, 리뷰 중 임시 config + fake CLI로 실제 `bin/edge.sh`와 `bin/node.sh`를 별도 프로세스로 실행해 보완 검증했다.

리뷰 중 재검증:
- `gofmt -l apps/node/cmd/node packages/hostsetup` → 출력 없음.
- `go test -count=1 -v ./apps/node/cmd/node` → PASS.
- `go test -count=1 ./apps/node/cmd/node ./packages/hostsetup ./apps/node/internal/bootstrap ./apps/node/internal/transport ./apps/node/internal/node` → PASS.
- `go test ./...` → PASS.
- `go run ./apps/node/cmd/node setup --help` → Global `--config` default가 `/etc/iop/node.yaml`.
- `go run ./apps/node/cmd/node config check` → `OK configs/node.yaml`.
- `go run ./apps/node/cmd/node setup --dry-run --binary /usr/local/bin/iop-node` → config/unit가 `/etc/iop/node.yaml` 기준.
- `go run ./apps/node/cmd/node --config /tmp/node.yaml setup --dry-run --binary /usr/local/bin/iop-node` → `/tmp/node.yaml` 반영.
- `go run ./apps/node/cmd/node setup --config /tmp/node.yaml --dry-run --binary /usr/local/bin/iop-node` → `/tmp/node.yaml` 반영.
- `rg --sort path -n "03\\+node_setup_cli|구현 예정" docs apps/node/README.md` → 출력 없음.
- `rg --sort path -n "service install|service status|\\brender\\b" docs apps/node/README.md` → 모두 금지/비공식 경로 문맥.

Bin shell 사용자 흐름 재검증:
- 임시 edge/node config와 persistent fake CLI를 사용하고, repo 기본 `configs/*.yaml`은 수정하지 않았다.
- `bin/edge.sh` 실행 후 `bin/node.sh`를 별도 프로세스로 실행했고 edge console에 `[node0-evt] connected reason="registered"`가 표시됨을 확인했다.
- `/nodes` → `node0 = review-node (review-node)` 확인.
- 메시지 1: `[edge] sent`, `[node0-evt] start`, `[node0-msg] IOP_REVIEW_ONE`, `[node0-msg] IOP_REVIEW_ONE_TAIL`, `[node0-evt] complete` 확인.
- 메시지 2: `[edge] sent`, `[node0-evt] start`, `[node0-msg] IOP_REVIEW_TWO`, `[node0-msg] IOP_REVIEW_TWO_TAIL`, `[node0-evt] complete` 확인.
- node 로컬 출력의 `[node-message]` payload가 edge의 `[node0-msg]` 출력과 일치함을 확인했다.
- `/capabilities`, `/transport`, `/sessions`, `/terminate-session` 응답 확인.

남은 위험: 없음.
