<!-- task=cli_runtime_closeout/01_node_command_contract plan=0 tag=API -->

# Plan - Node Command Response Contract

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수다. 구현 후 검증 명령을 실행하고 실제 stdout/stderr를 붙여 넣은 뒤, active 파일은 그대로 두고 review ready로 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

활성 Milestone은 `NodeCommandRequest` 운영 명령 응답을 운영자가 읽기 쉬운 payload로 마무리해야 한다. 현재 `SESSION_LIST`는 grouped rendering에 필요한 구조화 키를 갖지만 per-session label이 중복 계산되고, `TRANSPORT_STATUS`는 boolean `connected`만 있어 console/field smoke에서 상태 의미를 사람이 다시 해석해야 한다. `USAGE_STATUS`는 typed payload가 경계이므로 proto 변경 없이 parse metadata와 envelope를 테스트로 고정한다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/cli-automation-runtime-stabilization.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `apps/node/internal/adapters/cli/cli.go`
- `apps/node/internal/adapters/cli/cli_internal_test.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/node_test.go`
- `proto/iop/runtime.proto`
- `apps/node/README.md`

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

- `SESSION_LIST` structured keys: 현재 `mode/target/session_id`는 `TestHandleSessionList_PopulatedSnapshot`가 검증하지만 per-session display label key는 없다.
- `TRANSPORT_STATUS`: 현재 `connected`, `node_id`, `adapter`, `session_id`는 검증하지만 사람이 읽는 상태 문자열(`state`)은 없다.
- `USAGE_STATUS`: raw-only/empty parse metadata는 lifecycle blackbox 쪽에 있지만, command contract에 가까운 focused internal test로 envelope와 typed payload를 고정할 필요가 있다.

### 심볼 참조

- renamed/removed symbols: none.

### 분할 판단

분할 정책을 먼저 평가했다. 이 작업 그룹은 node contract, edge console/smoke, boundary docs, milestone closeout이 서로 다른 검증과 책임 경계를 가지므로 split 한다.

- `agent-task/cli_runtime_closeout/01_node_command_contract`: node response payload contract. dependencies: none.
- `agent-task/cli_runtime_closeout/02+01_edge_console_smoke`: edge console/smoke는 finalized node payload에 의존한다.
- `agent-task/cli_runtime_closeout/03_history_event_boundary`: docs boundary confirmation. dependencies: none.
- `agent-task/cli_runtime_closeout/04+01,02,03_milestone_closeout`: 앞선 3개 subtask의 `complete.log` 이후 Milestone 체크를 닫는다.

### 범위 결정 근거

이 subtask는 `apps/node/internal/adapters/cli`, `apps/node/internal/node`, `apps/node/internal/runtime`의 command contract만 다룬다. `proto/iop/runtime.proto`의 field 추가와 `proto/gen/**` 생성물 변경은 제외한다. `CAPABILITIES`의 target map 재설계도 이번 Milestone의 남은 필수 항목이 아니므로 제외한다.

### 빌드 등급

build=`cloud-G07`, review=`cloud-G07`. 내부 command response 계약과 CLI automation 운영 표면에 닿는 protocol-adjacent 변경이며, 후속 field smoke가 이 payload를 소비한다.

## 구현 체크리스트

- [ ] [API-1] `SESSION_LIST` result에 `session.N.label`을 추가하고 focused test로 고정한다.
- [ ] [API-2] `TRANSPORT_STATUS` result에 사람이 읽는 `state` 필드를 추가하고 node test로 고정한다.
- [ ] [API-3] `USAGE_STATUS` typed payload/envelope parse metadata를 focused test로 고정한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 `CODE_REVIEW-cloud-G07.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] SESSION_LIST Structured Label

#### 문제

[apps/node/internal/adapters/cli/cli.go](/config/workspace/iop/apps/node/internal/adapters/cli/cli.go:320)는 top-level `sessions` 문자열에만 `mode:target/session_id` label을 보관한다. [apps/node/internal/adapters/cli/cli_internal_test.go](/config/workspace/iop/apps/node/internal/adapters/cli/cli_internal_test.go:397)는 structured fields를 검증하지만 label key는 없다.

#### 해결 방법

Before [apps/node/internal/adapters/cli/cli.go](/config/workspace/iop/apps/node/internal/adapters/cli/cli.go:324):

```go
for i, s := range snaps {
	prefix := fmt.Sprintf("session.%d.", i)
	result[prefix+"mode"] = s.mode
	result[prefix+"target"] = s.target
	result[prefix+"session_id"] = s.sessionID
}
```

After:

```go
for i, s := range snaps {
	prefix := fmt.Sprintf("session.%d.", i)
	result[prefix+"label"] = s.label()
	result[prefix+"mode"] = s.mode
	result[prefix+"target"] = s.target
	result[prefix+"session_id"] = s.sessionID
}
```

Add assertions beside [apps/node/internal/adapters/cli/cli_internal_test.go](/config/workspace/iop/apps/node/internal/adapters/cli/cli_internal_test.go:406) that each `session.N.label` equals the expected sorted label.

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

- [ ] `apps/node/internal/adapters/cli/cli.go`: add `session.N.label`.
- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: assert labels in populated and slash-in-session-id cases.

#### 테스트 작성

Write tests in `apps/node/internal/adapters/cli/cli_internal_test.go`.

- `TestHandleSessionList_PopulatedSnapshot`: assert all labels in sorted order.
- `TestHandleSessionList_SlashInSessionID`: assert `session.0.label == persistent:claude/team/a/b`.

#### 중간 검증

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

Expected: package passes freshly, no cache-only result required.

### [API-2] TRANSPORT_STATUS State Field

#### 문제

[apps/node/internal/node/node.go](/config/workspace/iop/apps/node/internal/node/node.go:251)는 `connected=true|false`만 반환한다. Operator output and smoke can be clearer if the payload also carries a readable `state=connected|disconnected`.

#### 해결 방법

Before [apps/node/internal/node/node.go](/config/workspace/iop/apps/node/internal/node/node.go:246):

```go
connected := "false"
if sessionConnected(sess) {
	connected = "true"
}
result := map[string]string{
	"node_id":    n.nodeID,
	"connected":  connected,
	"adapter":    req.GetAdapter(),
	"target":     req.GetTarget(),
	"session_id": normalizeSessionID(req.GetSessionId()),
}
```

After:

```go
connected := "false"
state := "disconnected"
if sessionConnected(sess) {
	connected = "true"
	state = "connected"
}
result := map[string]string{
	"node_id":    n.nodeID,
	"connected":  connected,
	"state":      state,
	"adapter":    req.GetAdapter(),
	"target":     req.GetTarget(),
	"session_id": normalizeSessionID(req.GetSessionId()),
}
```

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

- [ ] `apps/node/internal/node/node.go`: add `state` without removing `connected`.
- [ ] `apps/node/internal/node/node_test.go`: assert `state` in explicit and default-session transport tests.

#### 테스트 작성

Write tests in `apps/node/internal/node/node_test.go`.

- Extend `TestOnCommandRequest_TransportStatus`: expect `state` key present and either `connected` or `disconnected`.
- Extend `TestOnCommandRequest_TransportStatusDefaultsSessionID`: assert default session still mirrors `Result["session_id"]`.

#### 중간 검증

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

Expected: package passes freshly, no cache-only result required.

### [API-3] USAGE_STATUS Typed Payload Contract

#### 문제

[apps/node/internal/adapters/cli/cli.go](/config/workspace/iop/apps/node/internal/adapters/cli/cli.go:238) returns `UsageStatus` as the dedicated typed payload. That is correct for [proto/iop/runtime.proto](/config/workspace/iop/proto/iop/runtime.proto:102), but focused tests should pin the envelope and parse metadata near the command payload tests.

#### 해결 방법

No production code change is expected unless the new test exposes a gap. Add an internal test that builds a `CLI` with a fake `StatusChecker`, calls `HandleCommand`, and asserts:

- `RequestID`, `Type`, `Adapter`, `Target`, `SessionID` echo the request.
- raw-only response has `UsageStatus.Metadata["parse_status"] == "raw_only"`.
- structured metadata-only response does not get a synthetic `parse_status`.

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

- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`: add focused usage status command payload test.

#### 테스트 작성

Write `TestHandleUsageStatus_EnvelopeAndParseMetadata` in `apps/node/internal/adapters/cli/cli_internal_test.go`.

#### 중간 검증

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

Expected: package passes freshly, no cache-only result required.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/node/internal/adapters/cli/cli.go` | API-1 |
| `apps/node/internal/adapters/cli/cli_internal_test.go` | API-1, API-3 |
| `apps/node/internal/node/node.go` | API-2 |
| `apps/node/internal/node/node_test.go` | API-2 |

## 최종 검증

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

```bash
rg --sort path -n 'session\.[0-9]+\.label|\"state\"' apps/node/internal/adapters/cli apps/node/internal/node
```

Expected: Go tests pass freshly. `rg` output includes the new session label and transport state contract in source/tests.

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