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

# Plan - Failover audit와 context/budget 검증 보강

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

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

## 배경

두 번째 리뷰에서 selector/context/budget 기반과 집중 테스트는 추가되었고 전체 178개 suite도 통과했다. 그러나 확정된 quota failover의 실패 target audit 상태가 잘못 남고, logical context validator가 workspace 누락과 locator attempt 밖 artifact를 허용한다. stage budget 테스트도 helper 2회 호출에 머물러 계획된 10회/restart/runtime recovery lifecycle을 입증하지 못했다. live failover 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_G07_1.log`
- Archived review: `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/code_review_cloud_G08_1.log`
- Verdict: `FAIL`
- Findings: Required 3, Suggested 0, Nit 0
- Summary: provider-quota failover가 실패 target을 `unknown/eligible`로 남기고, context validator가 workspace 누락과 외부 artifact를 승인하며, budget 테스트가 실제 10회/restart recovery loop를 실행하지 않았다.
- Affected files: `scripts/select_execution_target.py`, `scripts/dispatch.py`, `tests/test_select_execution_target.py`, `tests/test_dispatch.py`
- Verification evidence: focused selector 3 tests, focused context/budget 2 tests, 전체 Python suite 178 tests, `py_compile`, `git diff --check`는 모두 PASS했다. 별도 재현은 quota audit와 context fail-closed 오류를 확인했다.
- 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/SKILL.md`
- `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/scripts/dispatch.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.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-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-contract/index.md`

### SDD 기준

- SDD 상태는 `[승인됨]`, 잠금은 해제되어 있고 사용자 결정 대기는 없다.
- S06은 qualified provider target 불가, ordered unused candidate, no-bounce와 PLAN/locator/normalized/raw/workspace logical context evidence를 요구한다.
- S07은 동일 work unit/stage의 target 전환·restart 공유, 다른 stage 분리, 성공 reset과 10번째 실패 blocker evidence를 요구한다.
- 이 follow-up 단독으로 live S09 integration을 완료하지 않으므로 `Roadmap Targets`는 두지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`를 적용한다.
- Edge-Node smoke 명령은 Python dispatcher 계약과 매칭되지 않아 사용하지 않는다.
- 외부 provider/quota 호출 없이 고정 snapshot, fake subprocess JSON stream, 임시 locator와 persistent `StateStore`를 사용한다.

### 재현과 테스트 공백

- `provider-quota` failover 결과에서 실패한 Claude candidate가 `quota_status=unknown`, `eligibility=eligible`, `rejection_reason=None`으로 남았다.
- 실제 workspace가 cwd일 때 workspace 필드가 없는 locator가 `Path("").resolve()` 때문에 통과했고, locator sibling이 아닌 임의 raw/normalized 파일도 package에 포함됐다.
- `DynamicFailoverBudgetTest`는 budget helper를 두 번 직접 호출할 뿐 `run_escalating()`, state 재open, 10번째 실패 terminal block을 실행하지 않는다.
- 기존 focused/전체 suite가 모두 PASS하므로 테스트가 위 경계를 포착하도록 강화해야 한다.

### 심볼 참조

- rename/remove 없음.
- `_failover()` 결과는 selector CLI와 후속 dispatcher integration이 소비한다.
- `build_context_package()`는 후속 target transition caller가 소비하며 `invoke()`가 만든 `locator.json`, `stream.log`, `normalized-output.log`와 결합된다.
- `StageFailureBudget`은 `run_escalating()`의 worker/review failure/reset 경로와 `StateStore.clear_blocked()`가 소비한다.

### 분할 판단

- split 정책을 먼저 평가했다. 세 Required는 S06/S07 기반 pair의 audit와 verification closure이며 동일 네 파일을 함께 사용한다.
- 추가 분할은 selector/context/budget 사이의 half-contract를 만들고 후속 `05`의 dependency만 늘리므로 현재 subtask의 단일 follow-up으로 유지한다.
- live selector failover invocation, completing target selfcheck, scheduler/throughput rollout은 기존 `05+04_dispatch_integration` 범위로 유지한다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`.
- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`.
- build/review closures의 `scope_closed`, `context_closed`, `verification_closed`, `evidence_trusted`, `ownership_closed`, `decision_closed`는 모두 `true`다. exact 재현, 네 source/test 범위, 승인 SDD와 deterministic unittest로 닫혔다.
- build route_basis=`local-fit`, capability_gap=`none`.
- build loop-risk matched signatures=`temporal_state,boundary_contract,structured_interpretation,variant_product`, triggered=`true`; ordered states는 failover/resume/restart/retry/success/block이며 selector/context/state 경계와 failure/quota/stage variant를 함께 검증한다.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/2/1`, final=`local-G08`, filename=`PLAN-local-G08.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`.

## 구현 체크리스트

- [ ] confirmed `provider-quota` failover가 실패 target만 exhausted/ineligible로 audit하고 다른 qualified failure의 quota 상태는 보존하도록 수정·검증한다.
- [ ] logical context validator가 task/workspace/PLAN과 locator attempt-local raw/normalized artifact identity를 fail-closed 검증하도록 수정한다.
- [ ] fake JSON `invoke()`로 normalized output 순서와 locator 경로를 확인하고 workspace/PLAN/raw/normalized/외부 경로 누락·불일치 matrix를 검증한다.
- [ ] 실제 `run_escalating()`과 재open한 `StateStore`로 target 전환 전후 10회 budget, stage 분리, 성공 reset, retry-blocked 보존을 검증한다.
- [ ] focused tests, 전체 Python suite, `py_compile`, `git diff --check`를 통과하고 CODE_REVIEW 구현 evidence를 채운다.

## 구현 항목

### [REVIEW_API-1] Provider quota failover audit 정합성

- 문제: `_failover()`는 모든 cloud candidate의 quota 상태를 snapshot만으로 다시 계산한다. `failure_class=provider-quota` 자체가 확정 evidence인데도 snapshot에 실패 target 행이 없으면 이전 target이 `unknown/eligible`로 남는다.
- 해결 방법:

```python
# Before: select_execution_target.py:599
status = _snapshot_status(type("Target", (), current)(), quota_snapshot)
```

```python
# After
if failure_class == "provider-quota" and _target_ref(current) == previous:
    status = "exhausted"
else:
    status = _snapshot_status(...)
```

  실패한 현재 target에만 `exhausted/ineligible/quota_exhausted`를 기록한다. `context-limit`, `model-unavailable`, `provider-stream-disconnect`는 quota evidence가 아니므로 snapshot 상태를 그대로 보존한다. used candidate/no-bounce는 유지한다.
- 수정 파일:
  - `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`
  - `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`
- 테스트: qualified failure class table에서 quota failure의 이전 candidate audit과 non-quota failure의 quota 비변경, available/unknown/exhausted next candidate, resume/no-bounce를 검증한다.

### [REVIEW_API-2] Logical context identity와 artifact provenance

- 문제: 빈 workspace 문자열이 cwd로 resolve되고 artifact는 존재 여부만 검사해 locator attempt 밖 파일도 승인한다. 현재 test는 파일을 수동 생성하고 normalized 누락 한 건만 검사해 `invoke()`의 artifact 생성과 순서를 검증하지 않는다.
- 해결 방법:

```python
# Before: dispatch.py:2825
Path(str(record.get("workspace", ""))).resolve() == workspace.resolve()
```

```python
# After
validate_non_empty_absolute_identity(record["workspace"], workspace)
validate_task_and_plan_identity(record, task)
require_exact_attempt_artifact(locator.parent, "stream.log", record["stream_log"])
require_exact_attempt_artifact(locator.parent, "normalized-output.log", record["normalized_output_log"])
```

  task/workspace/PLAN을 명시적 non-empty 값으로 비교하고 raw/normalized path는 resolved locator parent의 exact attempt artifact로 제한한다. same-Pi native path도 같은 attempt/session evidence인지 검증하고 cross-adapter package에는 포함하지 않는다.
- 수정 파일:
  - `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`
  - `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`
- 테스트: fake JSON subprocess를 `invoke()`로 실행해 locator의 raw/normalized 경로와 normalized display order를 확인한다. missing/mismatch workspace, task, PLAN, raw, normalized, artifact sibling escape, same-Pi native 누락과 cross-adapter native 비유출을 table로 검증한다.

### [REVIEW_API-3] Stage budget runtime lifecycle evidence

- 문제: helper 단위 2회 카운트는 `run_escalating()`이 persisted stage budget을 읽고 10번째 실패에서 block하며 성공 시 reset하는지 증명하지 못한다.
- 해결 방법:

```python
# Before: test_dispatch.py:5293
worker.record_failure(...)
worker.record_failure(...)
```

```python
# After
persist primary failure, reopen StateStore, continue alternate failures through
run_escalating(), assert the shared tenth failure blocks without another retry
```

  production helper를 우회하지 않는 async integration fixture를 추가한다. primary 1회와 alternate 9회가 같은 `work_unit_id|stage` key를 쓰고, review는 0에서 분리되며, 성공만 worker budget을 지운다. `clear_blocked()`는 blocker/counter만 지우고 execution decision/history/used candidates를 보존하는지 재open 전후 확인한다. 테스트가 production mismatch를 드러내면 이 항목 범위 안에서 최소 수정한다.
- 수정 파일:
  - `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` (테스트가 드러낸 runtime mismatch가 있을 때만)
  - `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`

## 의존 관계 및 구현 순서

1. REVIEW_API-1의 audit contract와 selector table test를 먼저 닫는다.
2. REVIEW_API-2에서 `invoke()` artifact와 validator를 같은 fixture로 연결한다.
3. REVIEW_API-3에서 persistent state와 actual recovery loop를 검증한 뒤 전체 suite를 실행한다.
4. 이 task PASS 뒤 `05+04_dispatch_integration`이 live initial/resume/failover invocation을 연결한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | REVIEW_API-1 |
