<!-- task=m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config plan=1 tag=REVIEW_NCDRAIN -->

# Plan - REVIEW_NCDRAIN

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고, 실제 구현 내용과 실제 stdout/stderr를 리뷰 stub에 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 archive, `complete.log`, roadmap 완료 처리는 code-review/runtime 책임이다. 선택된 Milestone의 `구현 잠금 > 결정 필요` 항목 때문에 구현이 막힐 때만 리뷰 stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 직접 만들지 않는다. 환경/secret/service 차단, 일반 범위 조정, 후속 에이전트가 닫을 수 있는 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

이 후속 루프는 `code_review_cloud_G08_0.log`의 Required 1건만 닫는다. 현재 구현은 `runtime.concurrency`를 먼저 live apply한 뒤 adapter registry start를 수행하므로, adapter apply 실패 시 Node admission limit만 새 값으로 남을 수 있다. SDD의 previous-state preservation과 `runtime-config` 완료 evidence를 만족하려면 refresh 실패 경로에서 runtime gate도 이전 상태를 유지해야 한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone lock decision이 실제 구현을 막을 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 템플릿을 따른다. 구현 에이전트는 직접 사용자에게 질문하지 않으며, code-review가 요청 타당성을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/plan_cloud_G08_0.log`
- Current archived review: `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/code_review_cloud_G08_0.log`
- Verdict: FAIL
- Required summary:
  - `apps/node/internal/node/node.go:345`: `OnConfigRefresh` applies `runtime.concurrency` to `globalGate` before `nextSet.Registry.Start(ctx)` can fail at `apps/node/internal/node/node.go:377`, leaving admission capacity changed after a failed refresh.
- Suggested issues: 없음
- Nit issues: 없음
- Affected files:
  - `apps/node/internal/node/node.go`
  - `apps/node/internal/node/node_test.go`
- Prior verification evidence:
  - Reviewer rerun: `go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters ./apps/edge/internal/configrefresh ./apps/edge/internal/bootstrap` PASS.
  - Reviewer rerun: `git diff --check` PASS.
  - Implementer evidence in archived review includes `go test -count=1 ./apps/node/...`, `go test -count=1 ./apps/edge/...`, and `git diff --check` PASS.
- Roadmap carryover:
  - Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
  - Task ids remain `drain`, `runtime-config`.
  - The follow-up must close SDD S15 `runtime-config` without regressing S14 `drain`.
- Narrow reread allowed:
  - `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/code_review_cloud_G08_0.log`
  - `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/plan_cloud_G08_0.log`

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `drain`: 진행 중 run이 있는 adapter/provider 변경은 drop하지 않고 기존 run은 이전 adapter snapshot으로 완료시키며, 후속 request는 새 config 기준으로 라우팅한다. 검증: in-flight run 중 refresh 시 run completion과 후속 request routing이 기대대로 동작한다.
  - `runtime-config`: runtime concurrency와 workspace root 변경의 live apply 가능 여부가 구현되고, 불가능한 변경은 restart_required로 보고된다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/plan_cloud_G08_0.log`
- `agent-task/m-runtime-reconnect-config-refresh/06+05_node_drain_runtime_config/code_review_cloud_G08_0.log`
- `agent-task/archive/2026/06/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/complete.log`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/run_manager.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/node/node_concurrency_integration_test.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/cli/cli.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/runtime_test.go`
- `apps/edge/internal/configrefresh/classify.go`
- `apps/edge/internal/configrefresh/classify_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/transport/server.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- 상태: 승인됨, SDD 잠금 해제.
- 대상 Acceptance Scenario: S14 / `drain`, S15 / `runtime-config`.
- Evidence Map: S14는 in-flight run refresh integration test와 previous adapter snapshot completion plus follow-up routing을 요구한다. S15는 runtime concurrency/workspace refresh tests와 live or restart_required behavior verified를 요구한다.
- 이 follow-up은 S15의 실패 원상유지 경계를 보강한다. `runtime.concurrency` live apply가 성공 refresh에서만 commit되고 실패 refresh에서는 이전 admission state를 보존해야 S15 evidence가 완결된다.

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md`는 local checkout의 Go quick check를 우선하고, 실행 경로 변경 시 필수 검증 누락을 차단 사유로 기록하라고 한다.
- `agent-test/local/node-smoke.md`는 `apps/node/**` 변경 시 변경 패키지 또는 `go test ./apps/node/...`를 요구한다.
- `agent-test/local/edge-smoke.md`는 Edge 변경 시 `go test ./apps/edge/...`를 요구한다. 이 follow-up은 Edge 소스 변경을 의도하지 않지만 기존 archived scope가 Edge 결과와 연결되어 있어 최종 회귀에 포함한다.
- `agent-test/local/platform-common-smoke.md`는 config/proto 변경 시 적용된다. 이 follow-up은 config/proto 원본 변경을 포함하지 않는다.
- Go test cache output은 허용하지 않는다. fresh run을 위해 `-count=1`을 사용한다.

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

- 기존 구현은 concurrency 증가/감소 성공 경로, workspace restart_required, drain/deferred stop, Edge classifier를 테스트한다.
- 공백: `runtime.concurrency`와 adapter diff가 같은 refresh payload에 있고, adapter registry start가 실패할 때 concurrency gate가 이전 값으로 유지되는 regression test가 없다.

### 심볼 참조

- rename/remove 없음.
- 새 exported API 추가는 불필요하다.

### 분할 판단

- 기존 split subtask `06+05_node_drain_runtime_config` 안의 후속 루프다.
- predecessor `05`는 `agent-task/archive/2026/06/m-runtime-reconnect-config-refresh/05+04_node_adapter_diff/complete.log`로 충족되었다.
- Required가 하나의 Node atomicity bug로 좁혀져 추가 split은 불필요하다.

### 범위 결정 근거

- 포함: `OnConfigRefresh`의 runtime concurrency commit/rollback ordering, 실패 경로 regression test.
- 제외: Edge classifier/result 변경, workspace root 정책 변경, deferred stop 구조 변경, field smoke, docs, roadmap 문서 갱신.
- unrelated working tree의 `agent-roadmap/phase/automation-runtime-bridge/**` 변경은 이 review follow-up 범위가 아니다.

### 빌드 등급

- lane/GNN: `local-G06`.
- 이유: 단일 Required issue이며 bounded Node shared-state fix와 deterministic Go regression test로 판단 가능하다.

## 구현 체크리스트

- [ ] `OnConfigRefresh`에서 `runtime.concurrency`를 adapter registry start/swap 성공 이후에 commit하거나, 실패 경로에서 이전 `globalGate` capacity로 rollback한다. 검증: adapter start 실패 refresh 후 admission limit이 이전 값으로 유지된다.
- [ ] `apps/node/internal/node/node_test.go`에 `TestConfigRefreshFailedAdapterStartDoesNotApplyRuntimeConcurrency`를 추가한다. 검증: concurrency=1 상태에서 active run 중 concurrency=2 + failing CLI persistent profile refresh가 `FAILED`를 반환하고, 두 번째 concurrent run은 계속 `ErrConcurrencyLimitExceeded`로 reject된다.
- [ ] 대상 회귀로 `go test -count=1 -run 'TestConfigRefresh(FailedAdapterStartDoesNotApplyRuntimeConcurrency|UpdatesGlobalConcurrencyForFutureRuns|ConcurrencyDecreaseDoesNotCancelInFlight|WorkspaceRootReportsRestartRequired)' ./apps/node/internal/node`를 실행한다.
- [ ] 최종 회귀로 `go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters ./apps/edge/internal/configrefresh ./apps/edge/internal/bootstrap`와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

1. Regression test를 먼저 추가해 현재 실패를 확인한다.
2. `OnConfigRefresh` ordering/rollback을 수정한다.
3. 대상 회귀와 최종 회귀를 fresh run으로 실행한다.

### [REVIEW_NCDRAIN-1] Runtime Concurrency Rollback On Failed Refresh

#### 문제

`apps/node/internal/node/node.go:345`에서 `nextConcurrency`가 기존 capacity와 다르면 즉시 `n.globalGate.updateCapacity(nextConcurrency)`가 실행된다. 이후 `apps/node/internal/node/node.go:377`의 `nextSet.Registry.Start(ctx)`가 실패하면 response는 `FAILED`지만, global admission gate는 이미 새 concurrency를 들고 있다.

Before `apps/node/internal/node/node.go:345`:

```go
nextConcurrency := int(rc.GetConcurrency())
if prev := n.globalGate.currentCapacity(); nextConcurrency != prev {
	n.logger.Info("config refresh: applying runtime concurrency",
		zap.Int("previous", prev),
		zap.Int("next", nextConcurrency),
	)
	n.globalGate.updateCapacity(nextConcurrency)
}
```

#### 해결 방법

Runtime diff는 먼저 계산만 하고 mutation은 성공 commit 지점으로 늦춘다. Adapter diff가 없으면 기존처럼 바로 applied로 반환하기 전에 concurrency를 적용하고 `currentConfigSet.Runtime`을 갱신한다. Adapter diff가 있으면 `nextSet.Registry.Start(ctx)` 성공, router swap, adapter gate update 직후 `n.globalGate.updateCapacity(nextConcurrency)`를 적용하고 `n.currentConfigSet = nextSet`까지 같은 성공 경로에서 완료한다. 대안으로 선적용을 유지한다면 `prevConcurrency`를 저장하고 `nextSet.Registry.Start(ctx)` 실패 전후의 모든 실패 return에서 rollback해야 한다.

After example:

```go
nextConcurrency, hasRuntimeConcurrencyChange := 0, false
if rc := req.GetConfig().GetRuntime(); rc != nil {
	nextConcurrency = int(rc.GetConcurrency())
	hasRuntimeConcurrencyChange = nextConcurrency != n.globalGate.currentCapacity()
}

// ... after nextSet.Registry.Start(ctx) succeeds and before returning APPLIED:
if hasRuntimeConcurrencyChange {
	n.globalGate.updateCapacity(nextConcurrency)
}
n.currentConfigSet = nextSet
```

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

- [ ] `apps/node/internal/node/node.go`: runtime concurrency mutation을 failed refresh path 밖으로 이동하거나 rollback 처리.
- [ ] `apps/node/internal/node/node_test.go`: failing CLI persistent profile로 `Registry.Start` 실패를 재현하는 regression test 추가.
- [ ] 기존 `TestConfigRefreshUpdatesGlobalConcurrencyForFutureRuns`, `TestConfigRefreshConcurrencyDecreaseDoesNotCancelInFlight`, `TestConfigRefreshWorkspaceRootReportsRestartRequired`를 유지한다.

#### 테스트 작성

- 추가: `TestConfigRefreshFailedAdapterStartDoesNotApplyRuntimeConcurrency`.
- Fixture: `newQueuedSlowAdapter("slow", 0, 0, 0)`으로 active run을 잡고 global concurrency=1을 채운다. Refresh payload는 `Runtime{Concurrency:2}`와 autostart persistent CLI profile을 포함하되 command는 존재하지 않는 path로 둬 `Registry.Start` 실패를 만든다.
- Assertion: refresh response는 `FAILED`; active run이 유지되는 동안 두 번째 run은 `ErrConcurrencyLimitExceeded`로 reject되어 이전 global concurrency=1이 보존됨을 증명한다.

#### 중간 검증

```bash
go test -count=1 -run 'TestConfigRefresh(FailedAdapterStartDoesNotApplyRuntimeConcurrency|UpdatesGlobalConcurrencyForFutureRuns|ConcurrencyDecreaseDoesNotCancelInFlight|WorkspaceRootReportsRestartRequired)' ./apps/node/internal/node
```

기대 결과: 새 regression과 기존 runtime config tests PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/node.go` | REVIEW_NCDRAIN-1 |
| `apps/node/internal/node/node_test.go` | REVIEW_NCDRAIN-1 |

## 최종 검증

```bash
go test -count=1 -run 'TestConfigRefresh(FailedAdapterStartDoesNotApplyRuntimeConcurrency|UpdatesGlobalConcurrencyForFutureRuns|ConcurrencyDecreaseDoesNotCancelInFlight|WorkspaceRootReportsRestartRequired)' ./apps/node/internal/node
go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters ./apps/edge/internal/configrefresh ./apps/edge/internal/bootstrap
git diff --check
```

기대 결과: Node failed-refresh rollback regression, runtime config refresh tests, targeted Node/Edge packages, whitespace check가 모두 PASS. Go test cache output은 허용하지 않는다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
