<!-- task=m-agent-task-runtime-target-selector/04+03_failover_budget plan=1 tag=REVIEW_API -->

# Plan - Failover context와 공통 실패 예산 구현 재개

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

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

## 배경

첫 리뷰에서 신규 구현과 필수 evidence가 전혀 제출되지 않았고, selector는 여전히 failover를 거부한다. 선행 route-pin 상태는 PASS 완료되었으므로 이 follow-up은 SDD S06/S07의 failover, logical context, stage budget 기반을 구현한다. live dispatcher call-site 전환은 후속 `05+04_dispatch_integration`에 남긴다.

## Archive Evidence Snapshot

- Task: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget`
- Archived plan: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/plan_local_G08_0.log`
- Archived review: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/code_review_cloud_G09_0.log`
- Verdict: `FAIL`
- Findings: Required 4, Suggested 0, Nit 0
- Summary: failover selector, logical context package, stage 공통 실패 예산과 두 집중 테스트가 모두 없고 구현 evidence도 비어 있었다.
- Affected files: `scripts/select_execution_target.py`, `scripts/dispatch.py`, `tests/test_select_execution_target.py`, `tests/test_dispatch.py`
- Verification evidence: 두 집중 테스트 명령은 누락된 test class로 ERROR, 전체 기존 Python suite는 173 tests PASS, `py_compile`과 `git diff --check`는 exit 0.
- Roadmap carryover: S06 `context-failover`, S07 `failure-budget`; 이 subtask는 기반만 제공하며 Milestone Task 완료 체크는 `05+04_dispatch_integration`이 소유한다.

## 분석 결과

### 읽은 파일

- `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`
- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`
- `agent-test/local/rules.md`
- `agent-test/local/testing-smoke.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-contract/index.md`

### SDD 기준

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- S06 `context-failover`: qualified provider target 불가, 단방향/no-bounce, logical context evidence를 REVIEW_API-1/2와 selector/context 집중 테스트에 반영한다.
- S07 `failure-budget`: 동일 work unit/stage의 target 전환·restart 공유, stage 분리, 성공 reset evidence를 REVIEW_API-3과 budget 집중 테스트에 반영한다.
- 이 기반 subtask 단독 PASS로 Milestone Task 완료를 주장하지 않으므로 `Roadmap Targets`는 두지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`를 적용했다.
- `agent-test/local/testing-smoke.md`도 확인했으나 Edge-Node dev/smoke 명령은 agent-task dispatcher 기반 계약과 매칭되지 않는다.
- 현재 checkout의 Python unittest manifest를 fallback source로 사용하고 외부 provider/quota 호출 없이 고정 snapshot과 임시 state/locator를 쓴다.
- 필수 명령은 두 집중 test class, fresh-process 전체 Python suite, `py_compile`, `git diff --check`다. 별도 test-rule 유지보수는 필요하지 않다.

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

- `SelectorFailoverContractTests`가 없어 failover 명령이 class lookup ERROR로 끝난다.
- `DynamicFailoverBudgetTest`가 없어 context artifact와 stage budget 명령이 class lookup ERROR로 끝난다.
- 기존 173개 suite는 PASS하지만 `failover` 거부를 정상 계약으로 검증하므로 신규 S06/S07을 커버하지 않는다.

### 심볼 참조

- rename/remove 없음.
- selector public call은 `select_execution_target.py` CLI와 `dispatch.py`의 `_selector_module()`/`select_execution_decision()` bridge가 소비한다.
- locator `stream_log`, `native_session_path`와 recovery state는 `dispatch.py`의 invoke/recovery 및 `test_dispatch.py` fixture가 소비한다.

### 분할 판단

- split 정책을 먼저 평가했다. shared group은 `m-agent-task-runtime-target-selector`이고 현재 경계는 `03 → 04 → 05`다.
- predecessor `03+01,02_route_pin_state`는 `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/03+01,02_route_pin_state/complete.log`로 충족됐다.
- selector failover, context artifact, stage budget은 후속 `05`가 함께 소비하는 하나의 기반 계약이다. 다시 나누면 half-contract 상태와 추가 runtime dependency만 생기므로 현재 subtask를 단일 follow-up으로 유지한다.
- broad call-site rollout은 이미 `05+04_dispatch_integration`으로 분리되어 있다.

### 범위 결정 근거

- `run_worker`/`run_review`의 실제 failover invocation 전환, scheduler drain, selfcheck, throughput 정책은 후속 sibling 범위라 제외한다.
- quota producer, external CLI 호출, roadmap/spec/contract 문서 갱신은 제외한다.
- 선행 route-pin diff는 보존하고 이 task의 네 계획 파일에만 필요한 동작을 추가한다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`
- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`.
- build closures: `scope_closed=true`, `context_closed=true`, `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`; 승인 SDD, exact source/test 범위, 재현된 missing-class 오류와 deterministic unittest 명령으로 닫혔다.
- review closures: 위 여섯 값 모두 `true`; 네 source/test와 actual command output만으로 판정 가능하다.
- build route_basis=`local-fit`; capability_gap=`none`.
- build loop-risk: ordered states=6, adverse paths=`resume/failover/quota block/retry/success reset/retry_blocked`; actor count=1 dispatcher와 restart persistence; boundary components=selector/dispatcher/state/context artifact, consumers=dispatcher/tests; variant axes=initial target/quota/failure/stage. matched signatures=`temporal_state,boundary_contract,variant_product`, triggered=`true`.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/1/1`, final=`local-G07`, filename=`PLAN-local-G07.md`.
- review route_basis=`official-review`; capability_gap=`none`.
- review scores=`2/2/1/2/1`, final=`cloud-G08`, filename=`CODE_REVIEW-cloud-G08.md`; execution=`codex/gpt-5.6-sol xhigh`.

## 구현 체크리스트

- [ ] qualified provider target 불가만 단방향 failover로 처리하고 used candidate/no-bounce 및 quota `available|unknown|exhausted`를 `SelectorFailoverContractTests`로 검증한다.
- [ ] PLAN/locator/normalized output/raw log/workspace를 보존하는 logical context package와 same-Pi native/cross-adapter logical 경계를 구현해 `DynamicFailoverBudgetTest`의 context case를 통과한다.
- [ ] 동일 work unit/stage 실패 예산 10회를 target 전환·restart 전후에 공유하고 성공만 reset하며 `retry_blocked`가 decision/history를 보존하도록 구현해 budget case를 통과한다.
- [ ] 전체 Python suite, `py_compile`, `git diff --check`를 실행해 기존 회귀와 정적 검증을 통과한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REVIEW_API-1] Failover selector 계약

- 문제: `select_execution_target.py:597`이 모든 `failover`를 `unsupported_transition`으로 거부하고 `test_select_execution_target.py:283`도 거부를 기대한다.
- 해결 방법:

```python
# Before: select_execution_target.py:597
if transition == "failover":
    raise SelectorInputError("unsupported_transition", ...)
```

```python
# After
if transition == "failover":
    return _failover(prior_decision, failure_class=failure_class,
                     quota_snapshot=quota_snapshot, evaluated_at=evaluated_at)
```

  normalized `failure_class`를 명시 입력으로 받아 quota/rate-limit, context/output limit, model unavailable, provider transport disconnect만 qualified trigger로 허용한다. prior identity/stage/candidate schema를 재검증하고 이미 사용한 candidate와 이전 target을 제외한다. `unknown`은 work unit에서 한 번만 시도하며 confirmed provider-quota 결과만 `exhausted`로 반영한다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`: failover input/schema/decision 구현.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: 기존 unsupported 테스트를 교체하고 `SelectorFailoverContractTests` 추가.
- 테스트 작성: day/night initial target, qualified/generic failure, available/unknown/exhausted, used candidate와 no-bounce를 table fixture로 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests -v` — 모든 case PASS, exit 0.

### [REVIEW_API-2] Logical context package

- 문제: `dispatch.py:2225-2276`의 locator는 raw stream만 기록하고 `dispatch.py:2571-2583`의 normalized output은 콘솔에만 출력한다. continuation은 locator/workspace 문자열만 넘겨 cross-adapter 검증 객체가 없다.
- 해결 방법:

```python
# Before: dispatch.py:2571
rendered, discovered = render_json_line(spec.cli, line)
```

```python
# After
for display_line in rendered:
    normalized_output_log.write(display_line + "\n")
context = build_context_package(task, locator_path, workspace,
                                previous_spec=previous, next_spec=next_spec)
```

  attempt-local `normalized-output.log`를 만들고 locator에 경로를 영속화한다. builder/validator는 PLAN, locator, normalized artifact, raw log, workspace의 존재와 일치를 fail-closed 검증한다. same-Pi만 `resume_mode=native`와 native session을 허용하고 adapter가 바뀌면 `resume_mode=logical`로 native session을 제거한다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: artifact 기록과 package builder/validator.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: normalized order, 누락 path/workspace mismatch, native-session 유출 회귀.
- 테스트 작성: fake JSON stream과 temp locator/state를 사용해 정상 package와 fail-closed boundary를 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest -v` — context case PASS.

### [REVIEW_API-3] Stage 공통 실패 예산

- 문제: `dispatch.py:2798-2934`는 role counter를 직접 조작하고 selector transition/context evidence와 연결된 budget helper가 없다.
- 해결 방법:

```python
# Before: dispatch.py:2800
persisted = store.task_state(task).get("recovery_failures", {})
recovery_failures = int(persisted.get(role, 0))
```

```python
# After
budget = StageFailureBudget(store, task, stage=role)
budget.record_failure(target=spec, transition=transition)
```

  task plan/tag 세대가 work unit identity를 제공하고 stage별 counter를 공유한다. qualified primary failure와 alternate failure가 같은 10회 budget을 쓰며 target 전환/restart는 reset하지 않는다. 해당 stage 성공만 reset하고 `clear_blocked()`는 blocker/counter만 지우며 `execution_decisions`와 `route_transition_history`를 유지한다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: budget read/increment/reset 및 transition evidence helper.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: `DynamicFailoverBudgetTest`의 10회/restart/stage/reset/retry-blocked case.
- 테스트 작성: qualified primary 1회와 alternate 9회, generic same-target, restart, worker 성공 reset, review stage 격리, retry-blocked history 보존을 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest -v` — 모든 case PASS, exit 0.

## 의존 관계 및 구현 순서

1. archived predecessor `03+01,02_route_pin_state/complete.log`가 충족되어 있다.
2. REVIEW_API-1과 REVIEW_API-2를 구현한 뒤 REVIEW_API-3이 transition/context evidence를 사용하게 한다.
3. 이 task PASS 뒤 `05+04_dispatch_integration`이 live invocation을 연결한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | REVIEW_API-1 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REVIEW_API-2, REVIEW_API-3 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | REVIEW_API-1 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REVIEW_API-2, REVIEW_API-3 |

## 최종 검증

- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests -v` — PASS, exit 0.
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest -v` — PASS, exit 0.
- `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/scripts/execution_target_policy.py agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` — exit 0.
- `git diff --check` — 출력 없음, exit 0.

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