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

# Plan - no_promotion_target 연속 복구 backoff 진행

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

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

## 배경

직전 구현은 selector-backed worker가 canonical `no_promotion_target` 뒤 legacy `promoted_spec()`으로 내려가는 결함을 제거해 actual invocation과 persisted decision/history를 같은 target에 유지했다. 그러나 이 새 same-target recovery 분기는 sleep 지수에 `terminal_recovery_retries`를 사용하면서 counter를 증가시키지 않는다. 연속 provider failure에서 shared failure budget은 진행하지만 delay는 매번 1초에 머무르므로, 다른 terminal recovery 분기와 같은 counter 전이를 적용하고 연속 실패 회귀로 고정한다.

## 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_local_G06_2.log`
- Archived review: `agent-task/m-agent-task-runtime-target-selector/06+05_runtime_quota_failover/code_review_cloud_G06_2.log`
- Verdict: `WARN`; Required 0, Suggested 1, Nit 0.
- Suggested summary: selector-backed `no_promotion_target` 분기가 `terminal_recovery_retries`를 증가시키지 않아 연속 same-target recovery의 exponential backoff가 `[1, 1]`로 고정된다.
- Affected files: `dispatch.py`, `test_dispatch.py`.
- Verification evidence: focused dispatcher 31 tests와 전체 Python 212 tests, `py_compile`, `git diff --check`는 PASS했다. 별도 deterministic local-G08 연속 `provider-connection` 재현은 invocation `agy -> agy -> agy`, selected/history 정합성과 성공 후 budget reset을 확인했지만 sleep이 `[1, 1]`임을 확인했다.
- 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-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은 허용되지 않은 target 전환 없이 현재 target 복구를 유지하도록 하고, S07은 stage 단위 누적 failure budget과 성공 reset을 요구하며, S09는 actual invocation과 persisted decision/history 정합성을 요구한다.
- 이번 변경은 target 선택이나 promotion 권한을 바꾸지 않고, 이미 canonical no-target으로 판정된 같은 target 복구의 시간 간격만 기존 terminal recovery 규칙과 일치시킨다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/testing-smoke.md`를 확인했다.
- testing-smoke의 live provider/full-cycle 명령은 hidden dispatcher retry counter를 결정적으로 관찰하지 못하므로 pass criterion으로 사용하지 않는다.
- 외부 provider나 secret 없이 mocked invocation과 `AsyncMock` sleep으로 실제 target, persisted state, delay sequence, stage budget reset을 함께 검증한다.

### 재현 및 테스트 공백

- `dispatch.py:3355-3373`은 `no_promotion_target`에서 `terminal_recovery_retries`를 증가시키지 않은 채 `2 ** min(terminal_recovery_retries, 5)`를 계산한다.
- 다른 terminal same-target 분기인 `dispatch.py:3324-3338`과 legacy no-next-target 분기인 `dispatch.py:3424-3437`은 counter 증가 뒤 sleep하므로 첫 두 delay가 2초, 4초로 진행한다.
- 현재 `test_no_promotion_target_keeps_same_target_and_persists_state`는 한 번 실패 후 성공하여 같은 target/state만 검증하고 연속 retry cadence를 관찰하지 않는다.
- 두 번 실패 후 성공하는 deterministic 재현은 호출과 state/budget 정합성은 유지하지만 sleep `[1, 1]`을 반환한다.

### 심볼 참조

- `terminal_recovery_retries`는 `dispatch.py:3013`에서 초기화되고 일반 terminal recovery와 legacy no-next-target 경로에서는 각 sleep 전에 증가한다.
- 결함 call-site는 `dispatch.py:3371`, 대응 회귀는 `test_dispatch.py:6046`이다.
- public symbol, selector policy, persistence schema, dependency 변경은 없다.

### 분할 판단

- 기존 split child `06+05_runtime_quota_failover`의 WARN 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`로 충족됐다.
- 한 counter 전이와 그 전이를 관찰하는 단일 회귀가 같은 recovery invariant를 공유하므로 nested subtask로 나누지 않는다.
- sibling active artifact와 dirty worktree는 사용자 소유로 보존하고 아래 두 파일 외 변경을 수정하지 않는다.

### 범위 결정 근거

- selector target matrix, quota snapshot, logical context, persisted decision schema, roadmap와 SDD는 변경하지 않는다.
- agent-spec index에는 dispatcher runtime에 매칭되는 living spec이 없고 agent-contract index에도 hidden selector runtime 계약이 없다. 승인 SDD, 현재 코드, 직전 review finding과 deterministic 재현을 source of truth로 사용한다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`.
- build closures: `scope_closed=true`(누락된 counter 증가와 delay 성공 조건이 고정), `context_closed=true`(runtime 분기와 회귀 test를 함께 처리), `verification_closed=true`(delay sequence와 focused/full tests로 판정), `evidence_trusted=true`(실제 branch를 실행한 deterministic 재현), `ownership_closed=true`(현재 split child와 두 파일로 소유권 고정), `decision_closed=true`(승인 SDD와 기존 terminal retry 규칙으로 결정 가능).
- 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/1/1`, final=`local-G05`, filename=`PLAN-local-G05.md`.
- review route_basis=`official-review`; capability_gap=`none`; scores=`1/1/1/1/1`, final=`cloud-G05`, filename=`CODE_REVIEW-cloud-G05.md`; target=`codex/gpt-5.6-sol xhigh`.
- loop-risk observations: ordered states는 failure record → counter 증가/backoff → same-target retry → success budget reset이며 adverse path는 반복 실패 후 shared budget block이다. dispatcher retry cadence와 persisted budget/state는 두 consumer가 동일 failure sequence를 해석하므로 `temporal_state`, `boundary_contract`가 matched되고 triggered=`true`다. concurrent actor는 dispatcher 1개이고 structured parser나 조합 variant 변경은 없다.
- G floor: `none`; 이전 lane/G와 반복 횟수는 입력과 판정에 사용하지 않았다.

## 구현 체크리스트

- [x] selector-backed `no_promotion_target` 분기에서 sleep 전에 `terminal_recovery_retries`를 증가시켜 기존 terminal recovery와 같은 bounded exponential backoff를 적용한다.
- [x] local-G08 회귀를 최소 두 번 연속 `provider-connection` 뒤 성공하도록 확장하고 같은 AGY target, unchanged selected/history, delay `[2, 4]`, 성공 후 stage failure budget reset을 검증한다.
- [x] focused dispatcher tests, 전체 Python suite, `py_compile`, `git diff --check`를 fresh output으로 검증한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REVIEW_REFACTOR-1] Canonical no-target recovery counter 진행

- 문제: canonical `no_promotion_target` 분기는 shared stage failure budget을 증가시키고 같은 target으로 복구하지만 local `terminal_recovery_retries`는 0에 고정되어 모든 sleep이 1초다.
- 해결 방법:
  - `no_promotion_target` explicit branch 안에서 banner/sleep 전에 `terminal_recovery_retries += 1`을 적용한다.
  - delay 식과 최대 30초 cap, `previous_locator`, selector decision/history, shared failure budget과 success reset 로직은 변경하지 않는다.
  - legacy `promoted_spec()` 경계와 다른 failure branch는 수정하지 않는다.
- 수정 파일 및 체크리스트:
  - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: canonical no-target same-target recovery counter를 진행한다.
  - [x] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: 연속 no-target failure의 target/state/backoff/budget reset 회귀를 추가한다.
- 테스트 작성:
  - existing local-G08 test의 mock invocation이 `provider-connection`, `provider-connection`, success를 반환하게 하고 세 호출 모두 AGY Gemini Medium인지 assert한다.
  - patched `asyncio.sleep.await_args_list`가 `[2, 4]`인지 assert한다.
  - 성공 뒤 `execution_decisions.worker.selected`, `route_transition_history`, `blocked`, `recovery_failures`, `StageFailureBudget.count()`가 성공 상태와 일치하는지 assert한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DispatcherCanonicalFailoverIntegrationTest DynamicFailoverBudgetTest ReviewRetryTest -v`가 PASS해야 한다.

## 의존 관계 및 구현 순서

1. `no_promotion_target` same-target recovery counter를 기존 terminal branch와 같은 순서로 증가시킨다.
2. 연속 실패 회귀를 확장해 delay와 state/budget invariants를 함께 고정한다.
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와 연속 backoff 회귀 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`의 구현 에이전트 소유 섹션을 채운다.
