<!-- task=m-agent-task-runtime-target-selector/06+05_runtime_quota_failover plan=2 tag=REVIEW_REFACTOR -->

# Plan - Selector-backed promotion의 legacy fallthrough 제거

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

구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다.

## 배경

후속 리뷰에서 quota snapshot shape와 canonical cloud promotion의 정상 경로는 통과했지만, selector-backed worker가 canonical `promotion` 대상 없음(`no_promotion_target`)을 받으면 legacy `promoted_spec()`으로 fallthrough하는 결함이 확인됐다. 이 경로는 실제 invocation만 Claude로 바꾸고 persisted decision/history와 logical continuation context는 Gemini 상태에 남긴다. selector-backed live worker의 모든 promotion 결과를 canonical 정책 안에서 종결하고 legacy locator recovery와 명확히 분리한다.

## Archive Evidence Snapshot

- Prior task: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover`
- Archived plan: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/plan_cloud_G09_1.log`
- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G09_1.log`
- Verdict: `FAIL`; Required 1, Suggested 0, Nit 0.
- Required summary: selector-backed worker가 canonical `no_promotion_target`을 삼킨 뒤 legacy `promoted_spec()`으로 승격해 실제 invocation과 persisted decision/history/logical context가 분리된다.
- Affected files: `dispatch.py`, `test_dispatch.py`.
- Verification evidence: Go quota test, selector focused 19 tests, dispatcher focused 12 tests, 전체 Python 209 tests와 `py_compile`은 PASS했지만 deterministic local-G08 daytime `provider-connection` 재현에서 호출은 `agy -> claude`, persisted selected는 Gemini, history는 `initial`, continuation prompt에는 logical context artifact가 없는 불일치를 확인했다.
- Roadmap carryover: 이 pair에는 `Roadmap Targets`가 없으므로 PASS 시 Milestone Task 자동 체크를 주장하지 않는다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py`
- `agent-test/local/rules.md`
- `agent-test/local/testing-smoke.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md`
- `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md`

### SDD 기준

- SDD는 `[승인됨]`, 잠금은 `해제`다.
- S06은 허용된 qualified failover/promotion과 no-bounce를, S07은 stage 단위 누적 failure budget을, S09는 actual invocation과 persisted decision/history의 일치를 요구한다.
- canonical transition 대상이 없다는 selector 결과는 legacy cross-target 승격 권한이 아니며, 현재 target의 recovery budget 진행 또는 terminal block으로만 이어져야 한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/testing-smoke.md`를 읽었다.
- testing-smoke의 Edge-Node full-cycle/live provider 명령은 agent-ops Python dispatcher의 hidden selector 분기를 직접 검증하지 않아 pass criterion으로 사용하지 않는다.
- 외부 provider, secret, service 없이 deterministic mocked invocation과 fresh-process Python tests로 실제 호출 spec, state, context, budget을 함께 검증한다.

### 재현 및 테스트 커버리지 공백

- `dispatch.py:3340-3406`의 selector-backed worker guard는 canonical promotion을 시도하지만 `no_promotion_target`만 삼키고 공통 legacy `promoted_spec()` 분기로 내려간다.
- local-G08 daytime Gemini decision에서 첫 invocation이 `provider-connection`을 반환하고 두 번째가 성공하도록 구성하면 실제 spec은 `agy -> claude`가 되지만 `execution_decisions.worker.selected`와 `route_transition_history`는 초기 Gemini 상태에 머문다.
- 기존 `test_generic_failure_stays_on_same_target`은 `generic-error`만 다루고, `provider-connection`이 `CLOUD_PROMOTION_FAILURES`를 거쳐 canonical no-target 뒤 legacy 승격하는 분기를 실행하지 않는다.
- 기존 cloud promotion tests는 canonical target이 존재하는 정상 전이만 다루며 chain exhaustion 뒤 legacy helper에 도달하지 않는다는 assertion이 없다.

### 심볼 참조

- `promoted_spec()` 참조는 `dispatch.py:1553`, `1678`, `3067`, `3406`, `4693`에 있다.
- 현재 결함은 live `run_escalating()`의 `dispatch.py:3406` call-site다. legacy locator reclassification/recovery와 dry-run recovery call-site는 유지하고 별도 근거 없이 제거하지 않는다.
- public symbol rename이나 새 dependency는 없다.

### 분할 판단

- 기존 split child `06+05_runtime_quota_failover`의 FAIL follow-up이며 선행 `05+04_dispatch_route_source`는 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/05+04_dispatch_route_source/complete.log`로 충족됐다.
- 한 dispatcher 분기와 대응 회귀 테스트가 하나의 invocation/state invariant를 공유하므로 nested subtask로 나누지 않는다.
- sibling active artifact와 dirty worktree는 사용자 소유로 보존하고 아래 명시 파일 외 변경을 수정하지 않는다.

### 범위 결정 근거

- quota producer/consumer, canonical policy matrix, target admission, selfcheck, roadmap 문서는 이번 결함 범위에서 변경하지 않는다.
- agent-spec index에는 agent-task dispatcher에 매칭되는 living spec이 없고 agent-contract index에도 hidden selector runtime 계약 문서가 없다. 승인 SDD, 현재 코드, 재현과 tests를 source of truth로 사용한다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`.
- build closures: `scope_closed=true`(legacy fallthrough call-site와 성공 조건 고정), `context_closed=true`(dispatcher와 회귀 test를 함께 처리), `verification_closed=true`(deterministic reproducer와 focused/full tests), `evidence_trusted=true`(실제 분기를 실행한 재현으로 불일치 확인), `ownership_closed=true`(현재 split child와 legacy recovery 경계 고정), `decision_closed=true`(승인 SDD와 기존 failure policy로 결정 가능).
- review closures: 같은 source/state/reproduction/verification evidence로 모두 `true`.
- build route_basis=`local-fit`; capability_gap=`none`; scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/1/1/2/1`, final=`local-G06`, filename=`PLAN-local-G06.md`.
- review route_basis=`official-review`; capability_gap=`none`; scores=`1/1/1/2/1`, final=`cloud-G06`, filename=`CODE_REVIEW-cloud-G06.md`; target=`codex/gpt-5.6-sol xhigh`.
- loop-risk observations: ordered transition은 canonical promotion 성공/no-target recovery/budget exhaustion/block을 포함하고 adverse path는 `provider-connection`과 promotion-chain exhaustion이다. dispatcher-selector-StateStore boundary는 actual invocation, persisted decision/history, prompt, budget의 복수 consumer 정합성을 가진다. failure class와 route/promotion position의 2개 독립 variant 축을 조합 검증해야 한다. matched signatures=`temporal_state`, `boundary_contract`, `variant_product`; triggered=`true`. concurrent actor는 dispatcher 1개이고 structured parser 변경은 없다. signature는 lane/G에 영향을 주지 않았다.
- G floor: `none`; 이전 lane/G와 반복 횟수는 입력과 판정에 사용하지 않았다.

## 구현 체크리스트

- [x] selector-backed live worker가 canonical `no_promotion_target` 뒤 legacy `promoted_spec()`으로 fallthrough하지 않게 하고, 동일 target recovery/budget exhaustion 또는 canonical block으로 종결하며 legacy recovery call-site를 보존한다.
- [x] local-G08 `provider-connection`과 canonical promotion chain exhaustion 회귀를 추가해 invocation target, persisted decision/history, logical context, stage failure budget 정합성을 검증한다.
- [x] focused dispatcher tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REVIEW_REFACTOR-1] Selector-backed promotion의 legacy fallback 차단

- 문제: `run_escalating()`은 selector-backed worker의 canonical promotion이 `no_promotion_target`을 반환할 때 이를 비오류처럼 삼킨 뒤 같은 함수의 legacy `promoted_spec(spec, codex_recovery_count)`로 내려간다. local multi-candidate route의 unqualified `provider-connection`은 canonical promotion 대상이 없지만 legacy AGY→Claude 승격이 실행되어 상태와 호출이 분리된다.
- 해결 방법:

```python
# Before: selector가 no target을 반환해도 아래 legacy 분기로 fallthrough
except ExecutionDecisionError as exc:
    if "no_promotion_target" not in str(exc):
        ...
next_spec = promoted_spec(spec, codex_recovery_count)
```

```python
# After: selector-backed worker가 promotion branch 전체를 소유
except ExecutionDecisionError as exc:
    if "no_promotion_target" in str(exc):
        # current spec을 유지하며 shared stage budget의 recovery/block 규칙 적용
        ...
        continue
    block_selector_error(...)

# legacy selector state가 없는 검증된 recovery path에서만 사용
next_spec = promoted_spec(spec, codex_recovery_count)
```

  selector-backed worker 여부를 한 번 판정해 canonical promotion 성공 시 context 검증→decision/history commit→다음 invocation 순서를 유지한다. canonical 대상 없음은 실제 `spec`, persisted selected/history와 prompt context를 바꾸지 않고 현재 target recovery로 진행하며 이미 증가한 shared stage failure budget이 limit에 도달하면 block한다. malformed/tampered selector 오류는 기존처럼 fail closed한다. legacy terminal reclassification/restart recovery와 selector state가 없는 역할의 기존 호출은 동작 보존한다.
- 수정 파일 및 체크리스트:
  - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: selector-backed live promotion을 terminal branch로 만들고 legacy fallback 경계를 명시한다.
  - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: `provider-connection` same-target recovery, promotion-chain exhaustion, persisted state/context/budget 및 legacy recovery 보존 회귀.
- 테스트 작성:
  - local-G08 daytime initial Gemini에서 첫 `provider-connection`, 다음 성공을 반환하고 호출이 `agy -> agy`인지, selected/history가 불필요하게 바뀌지 않는지, shared budget이 성공 후 reset되는지 assert한다.
  - canonical cloud chain의 마지막 target에서 qualified failure가 발생할 때 legacy target으로 이동하지 않고 같은 target recovery 또는 limit block으로 끝나는지 assert한다.
  - canonical promotion 대상이 있는 Claude→Terra/AGY→Claude→Terra logical context와 commit ordering, 검증된 legacy reclassification tests가 계속 PASS하는지 확인한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v`가 PASS해야 한다.

## 의존 관계 및 구현 순서

1. `run_escalating()`의 selector-owned/legacy promotion 경계를 분리한다.
2. 누락 분기와 legacy 보존 회귀를 추가한다.
3. focused/full 회귀와 정적 검증을 실행하고 review evidence를 채운다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REVIEW_REFACTOR-1 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REVIEW_REFACTOR-1 |

## 최종 검증

- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v` — canonical no-target, promotion/context/budget와 legacy recovery 회귀 PASS.
- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh-process 전체 suite PASS.
- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — exit 0.
- `git diff --check` — 출력 없음.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
