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

# Plan - Runtime quota probe와 failover state machine

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

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

## 배경

persisted route source가 실제 invocation에 연결된 뒤 bounded candidate quota를 runtime에서 관측하고, resume과 허용된 failover만 동일 state transition으로 갱신한다.

## 분석 결과

### 읽은 파일

- `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_execution_target_policy.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`

### SDD 기준

- 대상 Acceptance Scenario는 S05~S07과 S09다. 이 child는 quota observation, pinned resume, qualified failover와 transition history 범위를 소유한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`가 존재하며 끝까지 읽었다.
- agent-ops Python dispatcher에 매칭되는 `agent-test/local/*.md` profile은 없다. 현재 checkout의 Python test manifest를 fallback 근거로 삼아 fresh-process `unittest`, `py_compile`, `git diff --check`를 사용하며 test-rule 유지보수는 필요하지 않다.
- provider subprocess는 fake runner와 임시 workspace/state/locator로 대체하고 실제 cloud/local 호출은 하지 않는다.
- 계획 시점 전체 Python suite 기준선은 169 tests PASS다.

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

- selector에 `iop-node quota-probe` runtime 호출부와 target/profile required-cap argv 매핑이 없다.
- dispatcher recovery가 selector transition history와 runtime quota를 하나의 state machine으로 갱신하지 않는다.
- initial/resume/failover invocation target과 persisted history 일치 검증이 없다.

### 심볼 참조

- `select_execution_target.py:485-491`의 source-only probe 기록과 `dispatch.py:2690-2987` recovery/promotion call-site를 runtime probe와 persisted transition 소비자로 갱신한다.

### 분할 판단

- 원본 `05+04_dispatch_integration`의 두 번째 child다.
- `05+04_dispatch_route_source`가 producer이며, 이 child의 persisted transition 결과를 `07+06_context_review_recovery`가 소비한다.

### 범위 결정 근거

- canonical target별 quota probe spec, no-shell probe normalization, resume/failover/promotion state transition만 변경한다.
- context package, retry-blocked budget lifecycle, review recovery/audit output은 다음 child 범위다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`
- finalizer=`finalize-task-policy.sh`, finalizer_mode=`pair`; build/review status=`routed`.
- build closures: `scope_closed=true`(승인 SDD와 이 subtask 경계가 고정됨), `context_closed=true`(명시된 source/test 범위를 한 local 작업에서 유지 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`.
- review closures: `scope_closed=true`, `context_closed=true`(동일 source/test 및 구현 evidence로 판정 가능), `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`.
- build route_basis=`local-fit`; capability_gap=`none`.
- build loop-risk audit: matched signatures=`temporal_state`, `boundary_contract`, `variant_product`; 이 기록은 lane/G를 바꾸지 않는다.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/1/2`, final=`local-G08`, filename=`PLAN-local-G08.md`.
- review route_basis=`official-review`; official execution=`codex/gpt-5.6-sol xhigh`; capability_gap=`none`.
- review scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/2/2`, final=`cloud-G09`, filename=`CODE_REVIEW-cloud-G09.md`.

## 구현 체크리스트

- [ ] bounded candidate의 runtime quota probe가 target/profile required-cap argv로 실행되고 probe 오류는 `unknown` snapshot으로 decision에 남는다.
- [ ] resume은 시간·본문 변경에도 pinned target을 사용하고, 확정된 provider target 불가 또는 명시적 retry eligibility만 transition history와 invocation target을 변경한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REFACTOR-2] Runtime quota probe와 resume/failover state machine

- 문제: `select_execution_target.py:485-491`은 probe command를 source 문자열로만 기록하고 실제 호출하지 않으며, `dispatch.py:2690-2987` recovery는 selector transition history와 runtime quota를 하나의 state machine으로 갱신하지 않는다.
- 해결 방법:

```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:485
"source": quota_probe_command,
```

```python
# After
snapshot = probe_candidate_quota(
    target=candidate.target,
    adapter=candidate.adapter,
    required_caps=quota_probe_spec(candidate).required_caps,
    checked_at=evaluated_at,
)
```

  - bounded candidate를 실제 평가할 때 shell 없이 `iop-node quota-probe --target <canonical> --command <adapter> --required-cap ... --checked-at <RFC3339>`를 실행한다. policy-owned mapping은 Gemini 3.6 Flash Low/Medium/High에 `overall`과 해당 `model:<canonical>` cap을, Claude/Codex에 `overall`을 선언한다. local model candidate는 probe하지 않고 오류/parse failure는 같은 target의 `unknown` snapshot으로 정규화한다.
  - persisted prior decision을 `resume`에 전달한다. `local-G07~G08`은 Gemini 3.6 Flash (Medium) 단일 target이므로 Laguna failover를 평가하지 않고 confirmed exhausted 또는 provider target 불가를 task-local blocker로 기록한다. 그 밖의 process/generic 오류와 dispatcher state/work-log/control/user-review/cancel 오류는 같은 target recovery로 처리한다.
  - 기타 cloud worker의 기존 `agy → Claude → Codex` promotion은 보존하면서 normalized transition/history와 공통 stage budget에 기록한다.
  - selector 반환, budget increment, used-candidate/history append, 다음 invocation 준비를 동일 task-state update 안에서 처리한다. unknown candidate의 confirmed runtime `provider-quota`는 해당 work unit의 derived evidence만 exhausted로 바꾸고, generic stderr는 unknown을 유지한다. invalid/no-eligible와 confirmed exhausted는 task-local blocker다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py`: canonical target별 command/profile/required-cap probe spec.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`: no-shell subprocess/normalization과 injectable snapshot 경계.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: resume/failover/promotion orchestration과 task-local terminal state.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`: target별 probe spec matrix.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: probe argv, parse/error→unknown과 bounded/local matrix.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: restart/time/failure-class/failover/promotion integration.
- 테스트 작성: `SelectorDispatcherIntegrationTest`에서 day→night pin, body edit, new generation, qualified provider target 불가 failover, generic/Pi same-target recovery, excluded control error, available/unknown 1회 Gemini, confirmed exhausted blocker, 기존 cloud promotion을 검증한다. selector test에는 fake probe argv와 snapshot을 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v`와 `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v`가 PASS해야 한다.

## 의존 관계 및 구현 순서

1. `05+04_dispatch_route_source`의 `complete.log`가 PASS여야 한다.
2. 이 task의 transition/history 결과를 `07+06_context_review_recovery`가 소비한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-2 |

## 최종 검증

- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — resume/failover/promotion 회귀 PASS.
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v` — runtime probe와 selector 전체 회귀 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/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` — 출력 없음.

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

