<!-- task=m-agent-task-runtime-target-selector/01_selector_contract plan=3 tag=REVIEW_API -->

# Selector malformed 입력과 quota roundtrip 계약 완결

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

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

## 배경

세 번째 구현은 candidate/decision/quota 하위 schema를 보강했지만, unhashable `stage`/`lane`은 여전히 traceback과 exit 1을 만든다. 또한 잘못된 quota metadata와 빈 probe command가 initial 성공 JSON에 들어간 뒤 동일 결과의 resume에서 거부되어 selector 자기 출력 계약이 닫히지 않는다. SDD S01의 명시적 invalid-input 오류와 initial→resume roundtrip을 한 번에 완결한다.

## Archive Evidence Snapshot

- Prior task: `agent-task/m-agent-task-runtime-target-selector/01_selector_contract`
- Archived plan: `agent-task/m-agent-task-runtime-target-selector/01_selector_contract/plan_cloud_G07_2.log`
- Archived review: `agent-task/m-agent-task-runtime-target-selector/01_selector_contract/code_review_cloud_G09_2.log`
- Verdict: `FAIL`
- Findings: Required 2, Suggested 0, Nit 0
- Required:
  - `prior_decision.stage=[]`와 `lane={}`가 `malformed_prior_decision` 대신 raw `TypeError` traceback/exit 1로 종료된다.
  - 숫자 `quota_snapshot.snapshot_id`/`checked_at`, 배열 `source`, 빈 `quota_probe_command`가 resume 불가능한 initial 성공 JSON을 만들 수 있다.
- Affected files: `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`
- Verification evidence: selector/policy 통합 27개와 대상 파일 `py_compile`은 PASS했다. 집중 재현은 unhashable stage/lane의 exit 1과 잘못된 quota metadata의 initial 성공 후 `malformed_prior_decision` resume 거부를 확인했다.
- Roadmap carryover: `selector-contract`, `time-route`, `grade-route`; SDD S01의 완전한 selector JSON과 명시적 invalid-input 오류가 현재 차단 항목이다.
- 필요한 경우 위 두 archived log만 prior-loop 상세 근거로 읽는다. `agent-task/archive/**`를 탐색하지 않는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md)
- Task ids:
  - `selector-contract`: task file 기반 selector JSON 계약
  - `time-route`: KST 주야간 경계 route
  - `grade-route`: worker/review G01~G10 route matrix
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `.gitignore`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/common/philosophy.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/finalize-task-routing/SKILL.md`
- `agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh`
- `agent-ops/skills/common/plan/templates/review-stub-template.md`
- `agent-roadmap/current.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`
- `agent-spec/index.md`
- `agent-contract/index.md`
- `agent-test/local/rules.md`
- `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md`
- `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-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`
- `agent-task/m-agent-task-runtime-target-selector/01_selector_contract/plan_cloud_G07_1.log`
- `agent-task/m-agent-task-runtime-target-selector/01_selector_contract/code_review_cloud_G09_1.log`
- `agent-task/m-agent-task-runtime-target-selector/01_selector_contract/plan_cloud_G07_2.log`
- `agent-task/m-agent-task-runtime-target-selector/01_selector_contract/code_review_cloud_G09_2.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 없음
- 직접 대상: S01→`selector-contract`
- 회귀 대상: S02→`time-route`, S03→`grade-route`
- Evidence Map: S01은 완전한 selector schema와 invalid-input 단위 테스트를 요구하고, S02/S03은 KST 네 경계와 worker/review 전체 matrix 회귀를 요구한다.
- 구현 체크리스트는 S01의 malformed prior/quota 입력 정규화와 initial→resume self-roundtrip에서 역산했고, 최종 검증은 S02/S03 회귀를 함께 실행한다.

### 테스트 환경 규칙

- `test_env=local`
- `agent-test/local/rules.md`를 읽었다. `agent-ops/skills/project/**`에 매칭되는 추가 profile route는 없다.
- 외부 runner, provider, secret, port를 사용하지 않는다.
- fallback은 현재 checkout의 Python `unittest`와 `py_compile`이다. test-rule 유지보수는 필요하지 않다.
- 현재 증거: selector/policy 통합 27개와 대상 파일 `py_compile`은 PASS했지만 집중 malformed-input/roundtrip 재현은 실패한다.

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

- `prior_decision.stage`/`lane`의 unhashable JSON type이 direct API와 CLI에서 안정적인 selector 오류가 되는 회귀가 없다.
- quota snapshot의 `snapshot_id`, `source`, `checked_at` type과 빈 `quota_probe_command`가 initial 출력 전에 거부되는 회귀가 없다.
- 유효 quota snapshot을 포함한 initial 결과가 그대로 resume 가능한지 검증하는 roundtrip 회귀가 없다.
- candidate/decision/quota 기존 malformed variant, 정상 initial/resume, KST 네 경계와 worker/review grade matrix는 기존 테스트가 커버한다.

### 심볼 참조

- renamed/removed symbol: none

### 분할 판단

- split 정책을 파일 선택 전에 적용했다.
- prior identity type guard와 quota metadata 검증은 같은 selector 입력 validator가 “성공한 initial 결과는 즉시 resume 가능하다”는 하나의 S01 불변조건을 함께 닫는다.
- 동일 source와 전용 test 파일, 동일 direct/CLI 오류 envelope, 동일 roundtrip 검증을 분리하면 중복 fixture와 중간 불완전 계약이 생긴다. API-vs-call-site rollout, 다른 ownership, 외부 검증 환경도 없으므로 현재 `01_selector_contract`의 단일 follow-up이 안전하다.
- sibling `02+01_quota_input`의 required-cap tri-state/admission은 이 subtask `complete.log`를 기다리며 현재 범위에 포함하지 않는다.

### 범위 결정 근거

- quota required-cap tri-state, unknown 1회 admission과 target 재선택은 `02+01_quota_input` 소유이므로 수정하지 않는다.
- dispatcher persisted state, failover, failure budget, dependency drain은 후속 Epic 소유이므로 수정하지 않는다.
- `execution_target_policy.py`의 시간/grade matrix와 orchestration skill의 실행 route 표면은 변경하지 않는다.
- 추가 key 허용과 현재 target `status`의 graceful `unknown` 정규화는 유지한다. 이번 변경은 resume 불가능한 type을 성공 출력에 넣지 않는 입력 경계만 닫는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`
- finalizer: `finalize-task-policy.sh`
- finalizer_mode: `pair`
- build closures:
  - scope/context/verification/evidence/ownership/decision 모두 `true`
  - 근거: 한 selector validator와 전용 회귀로 범위가 닫히고, direct API/CLI 및 initial→resume 재현과 local unittest로 결과를 결정적으로 판별하며, 사용자 결정이나 외부 환경이 없다.
- build loop-risk raw facts:
  - ordered transitions: state_count=2(`initial`, `resume`), recovery 경로는 있으나 3-state temporal signature는 없다.
  - concurrent consistency: actor_count=1, 공유 lock/atomic/exactly-once 제약 없음.
  - boundary contract: selector 한 component를 direct API와 CLI 두 consumer가 사용하며 validation/compatibility를 함께 유지한다.
  - structured interpretation: JSON parser와 중첩 object/list/type 해석 위험이 있다.
  - variant product: prior identity, quota metadata/probe, direct/CLI 및 initial/resume 축의 조합 검증이 필요하다.
- build base route: `local-G04`
- build grade scores: scope_coupling=1, state_concurrency=1, blast_irreversibility=1, evidence_diagnosis=0, verification_complexity=1
- build loop-risk: `boundary_contract`, `structured_interpretation`, `variant_product`가 매칭되어 `triggered: true`; cloud minimum `G07` 적용
- build final: `cloud-G07`, `PLAN-cloud-G07.md`
- review closures:
  - scope/context/verification/evidence/ownership/decision 모두 `true`
  - 근거: active PLAN, SDD S01, selector source, passing 전체 suite와 failing 집중 재현을 한 checkout에서 대조할 수 있고 미해결 결정이 없다.
- review base route: `local-G05`
- review grade scores: scope_coupling=1, state_concurrency=1, blast_irreversibility=1, evidence_diagnosis=1, verification_complexity=1
- review capability floor: official review minimum `cloud-G09`
- review final: `cloud-G09`, `CODE_REVIEW-cloud-G09.md`

## 구현 체크리스트

- [ ] prior decision의 `stage`/`lane` type과 quota snapshot metadata/probe command를 success JSON 생성 전에 검증해 모든 malformed JSON을 안정적인 `SelectorInputError`/CLI exit 2 envelope로 정규화하고, 유효 initial 결과가 그대로 resume되는 direct API·CLI 회귀를 추가한다.
- [ ] `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*execution_target*.py'`와 대상 파일 `py_compile`을 실행해 S01~S03 전체 회귀가 통과함을 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_API-1] Prior identity type과 quota roundtrip 검증

#### 문제

`agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:304`는 `stage`/`lane` type guard 전에 set membership을 수행해 list/dict JSON이 raw `TypeError`로 샌다.

```python
# select_execution_target.py:304
if value["stage"] not in policy.VALID_STAGES:
    ...
if value["lane"] not in policy.VALID_LANES:
    ...
```

`agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:326`은 quota snapshot의 `targets[]`만 검사하고, `:405`는 top-level metadata를 그대로 출력한다. 이 때문에 selector가 만든 initial JSON이 새 prior validator를 통과하지 못할 수 있다.

```python
# select_execution_target.py:405
return {
    "snapshot_id": quota_snapshot.get("snapshot_id"),
    ...
    "source": quota_snapshot.get("source", quota_probe_command),
    "checked_at": quota_snapshot.get("checked_at"),
}
```

#### 해결 방법

- prior `stage`와 `lane`은 문자열인지 먼저 확인한 뒤 enum membership을 검사한다.
- quota snapshot의 optional `snapshot_id`/`checked_at`은 `null|string`, optional `source`는 non-empty string일 때만 허용한다. 누락은 현재 fallback/`None` 동작을 유지한다.
- `quota_probe_command`는 non-empty string으로 검증해 snapshot이 없거나 source가 생략된 cloud 출력도 유효한 `quota.source`를 보장한다.
- 위반은 `malformed_prior_decision`, `malformed_quota_snapshot`, 명시적 probe-command 입력 오류 중 해당 경계의 안정적인 `SelectorInputError`로 정규화한다.

```python
stage = value["stage"]
if not isinstance(stage, str) or stage not in policy.VALID_STAGES:
    raise SelectorInputError(...)

if "source" in snapshot:
    _require_non_empty_string(snapshot, "source", "quota_snapshot", code)
```

#### 수정 파일 및 체크리스트

- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` — prior identity type guard와 quota metadata/probe command validator를 추가한다.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` — unhashable prior, malformed quota metadata/probe, 유효 initial→resume roundtrip의 direct/CLI 회귀를 추가한다.

#### 테스트 작성

작성한다.

- `test_resume_rejects_unhashable_stage_and_lane_types`: valid initial을 복제해 stage=list, lane=dict를 주입하고 direct API가 `malformed_prior_decision`인지 확인한다.
- `test_quota_metadata_is_validated_before_initial_output`: numeric snapshot/checked time, empty·non-string source와 empty probe command가 success JSON을 만들지 않는지 확인한다.
- `test_valid_quota_initial_output_resumes`: optional metadata가 생략되거나 유효한 snapshot을 사용한 initial 결과가 그대로 resume되는지 확인한다.
- `test_cli_malformed_identity_and_quota_metadata_use_json_error_envelope`: 대표 prior/quota/probe 위반이 stdout 없이 exit 2와 JSON stderr인지 확인한다.
- fixture는 `TemporaryDirectory`의 task/prior/quota JSON만 사용한다.

#### 중간 검증

```bash
python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_select_execution_target.py'
```

기대 결과: 기존 nested variant와 새 prior/quota/probe malformed 입력이 안정적인 selector 오류가 되고, 모든 유효 initial JSON이 resume된다.

## 수정 파일 요약

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

## 최종 검증

```bash
python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*execution_target*.py'
python3 -m py_compile \
  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_execution_target_policy.py \
  agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py
```

기대 결과: S01의 완전한 selector JSON/invalid-input envelope와 S02/S03 시간·grade matrix가 모두 PASS하고 대상 Python 파일이 compile된다. Python unittest cache는 해당 없다.

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