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

# 실행 대상 Selector JSON 계약 구현

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

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

## 배경

KST 시간/grade 정책은 `execution_target_policy.py`의 순수 결정 모듈과 단위 테스트로 먼저 고정됐다. 아직 task 파일·PLAN header·prior decision을 입력으로 받아 안정적인 JSON을 반환하는 selector CLI가 없어 dispatcher가 해당 정책을 결정적 계약으로 소비할 수 없다. 승인된 SDD의 S01~S03을 selector 표면에서 닫고 quota probe가 연결될 자리를 고정한다.

## 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

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `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/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/finalize-task-routing/SKILL.md`
- `agent-roadmap/current.md`
- `agent-roadmap/priority-queue.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/scripts/execution_target_policy.py`
- `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`

### 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: selector schema/invalid-input 테스트, KST 네 경계 테스트, worker local/cloud G01~G10 및 CODE_REVIEW matrix snapshot을 요구한다.
- 구현 체크리스트는 S01의 task/header/JSON validation과 prior-decision determinism을 먼저 고정하고, S02/S03을 selector CLI를 통한 black-box matrix로 재검증하도록 구성했다.

### 테스트 환경 규칙

- `test_env=local`
- `agent-test/local/rules.md`를 읽었다. `agent-ops/skills/project/**`에 매칭되는 profile route는 없어 profile 문서는 적용하지 않는다.
- fallback은 저장소의 기존 Python `unittest` 구조와 직접 실행 가능한 `python3 -m unittest discover`다.
- 외부 runner, provider, secret, port는 사용하지 않는다. test-rule 유지보수는 필요하지 않다.
- 확인된 명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_execution_target*.py'`는 현재 정책 테스트 5개를 통과한다.

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

- `time-route`: 순수 정책의 `06:59:59`, `07:00:00`, `22:59:59`, `23:00:00` KST 경계는 현재 테스트가 커버한다. selector CLI를 통한 동일 경계 검증은 없다.
- `grade-route`: 순수 정책의 worker local/cloud G01~G10과 review matrix는 현재 테스트가 커버한다. task filename/header 파싱과 결합된 matrix는 없다.
- `selector-contract`: JSON schema, invalid filename/stage/header, stable `work_unit_id`, 동일 입력 결정성, resume pin 테스트가 전혀 없다.

### 심볼 참조

- renamed/removed symbol: none

### 분할 판단

- split 정책을 파일 선택 전에 적용했다.
- task group: `m-agent-task-runtime-target-selector`
- `01_selector_contract`: Python selector 계약과 S01~S03 selector-level 검증. 선행 작업 없음.
- `02+01_quota_input`: 기존 Go usage checker 기반 quota JSON probe. `01_selector_contract`의 `complete.log`에 의존한다.
- selector 계약과 quota probe는 Python policy/JSON 경계와 Node Go checker라는 서로 다른 ownership·검증 경계이므로 분리했다. `01_selector_contract` 내부는 하나의 CLI 계약, 하나의 주 ownership, 동일한 Python 검증 명령을 공유하므로 추가 분할은 인위적이다.

### 범위 결정 근거

- `dispatch.py`의 persisted state, failover, failure budget, selfcheck, dependency drain은 continuation/integration Epic 소유이므로 수정하지 않는다.
- 실제 Go quota checker 호출과 required-cap tri-state는 `02+01_quota_input`으로 분리한다.
- `finalize-task-routing`의 lane/G 판정과 공식 review lifecycle은 변경하지 않는다.
- selector는 generic stderr로 quota/provider 상태를 추정하지 않는다.

### 최종 라우팅

- evaluation_mode: `first-pass`
- finalizer: `finalize-task-policy.sh`
- finalizer_mode: `pair`
- build closures: scope/context/verification/evidence/ownership/decision 모두 `true`
- build closure basis: 새 selector와 기존 순수 policy/test로 범위가 닫히고, 로컬 unittest로 schema·invalid input·matrix를 결정적으로 검증하며, 사용자 결정과 외부 서비스가 없다.
- 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`; signature 개수는 grade에 더하지 않고 cloud minimum `G07`만 적용
  - `boundary_contract`: task filename/header, selector JSON, policy 및 후속 dispatcher consumer 사이의 validation/compatibility 계약
  - `structured_interpretation`: filename/header/JSON 해석과 명시 stage·resume pin·시간 정책의 precedence
  - `variant_product`: worker/review × local/cloud × G01~G10 및 KST 경계 조합 검증
- build final: `cloud-G07`, `PLAN-cloud-G07.md`
- review closures: scope/context/verification/evidence/ownership/decision 모두 `true`
- review closure basis: SDD, selector/policy source, black-box tests를 한 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`

## 구현 체크리스트

- [ ] task filename, PLAN header와 입력 옵션을 검증하고 SDD 출력 필드를 안정적인 JSON으로 반환하는 selector CLI를 구현한다.
- [ ] 동일 입력 결정성, invalid input, resume pin, KST 네 경계와 worker/review 전체 grade matrix를 selector CLI 수준에서 검증한다.
- [ ] `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_execution_target*.py'`를 실행해 전체 selector 정책/계약 테스트가 통과함을 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## [API-1] Selector CLI와 안정적인 JSON schema

### 문제

`agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py:64`의 `select_policy()`는 ordered target 정책만 반환한다. task 파일 필수 입력, stage/lane/grade 파싱, `task/plan/tag` 세대 identity, prior decision, quota envelope, transition evidence를 조합하는 CLI가 없다.

### 해결 방법

현재 기준:

```python
# execution_target_policy.py:64
def select_policy(
    *, stage: str, lane: str, grade: int, evaluated_at: datetime
) -> PolicyDecision:
```

`scripts/select_execution_target.py`를 추가하고 다음 계약을 고정한다.

```python
def select_execution_target(
    task_file: Path,
    *,
    stage: str | None,
    evaluated_at: datetime,
    transition: str,
    prior_decision: dict[str, object] | None,
    quota_snapshot: dict[str, object] | None,
    quota_probe_command: str,
) -> dict[str, object]:
    ...
```

- positional `task_file`은 `PLAN-(local|cloud)-GNN.md` 또는 `CODE_REVIEW-(local|cloud)-GNN.md`만 허용한다.
- `stage` 생략 시 prefix에서 `worker|review`를 판별하고 명시 stage가 prefix와 충돌하면 입력 오류로 종료한다.
- 첫 1KiB의 `<!-- task=... plan=N tag=... -->`를 검증해 body 수정과 무관한 `work_unit_id`를 만든다.
- `schema_version`, identity/stage/lane/grade, selected/candidates, decision/quota/transition을 항상 같은 key와 타입으로 출력한다.
- `initial`은 순수 policy를 사용하고 `resume`은 동일 work unit의 prior selected target을 시간 변화와 무관하게 pinned 상태로 반환한다.
- `failover` 상태 전이는 다음 Epic 소유임을 명시적 `unsupported_transition` 입력 오류로 남기고 임의 target 전환을 하지 않는다.
- quota probe가 아직 없거나 snapshot이 없으면 cloud candidate는 `unknown`, local candidate는 `unbounded/not_applicable`로 표현한다. raw provider stderr는 입력으로 받지 않는다.
- 오류는 stderr JSON과 non-zero exit로 반환하고 stdout에 부분 decision을 남기지 않는다.

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

- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` — CLI, validation, work-unit identity, JSON serialization을 추가한다.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` — 필요할 때만 JSON serialization용 명시 helper를 추가하고 정책 matrix는 바꾸지 않는다.

### 테스트 작성

작성한다. `tests/test_select_execution_target.py`에 valid task/header, stage inference/mismatch, invalid filename/header/grade, body-only 변경 identity 안정성, fixed clock 결정성, resume pin, JSON key/type 검증을 추가한다. fixture는 `TemporaryDirectory` 안의 task file과 주입 JSON만 사용한다.

### 중간 검증

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

기대 결과: selector contract 테스트가 모두 PASS하고 invalid 입력 케이스는 예상한 non-zero/오류 code를 검증한다.

## [API-2] Selector 표면의 시간/grade matrix evidence

### 문제

`agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py:24`는 순수 policy만 검증한다. S02/S03 Evidence Map이 요구하는 selector-level KST 경계와 task filename 기반 worker/review matrix snapshot이 없다.

### 해결 방법

현재 기준:

```python
# test_execution_target_policy.py:24
class ExecutionTargetPolicyTests(unittest.TestCase):
```

`test_select_execution_target.py`에서 실제 task filename/header fixture를 만들고 selector entry point를 호출한다.

- KST `06:59:59`, `07:00:00`, `22:59:59`, `23:00:00`을 timezone-aware ISO 입력으로 고정한다.
- worker `local|cloud × G01..G10` 전 조합의 canonical `adapter + target + execution_class + selfcheck_required`를 표로 검증한다.
- `CODE_REVIEW-local/cloud-G01..G10`은 모두 `codex + gpt-5.6-sol`, `cloud_model`, selfcheck false인지 검증한다.
- candidate rank는 1부터 연속이며 dynamic local G07~G08만 시간대에 따라 Gemini/Laguna 순서가 바뀌는지 확인한다.

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

- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` — S01~S03 black-box matrix를 추가한다.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` — 순수 policy 기대값과 selector 기대값이 중복 상수가 되지 않도록 필요한 최소 helper만 정리한다.

### 테스트 작성

작성한다. `test_kst_boundary_routes_through_selector`, `test_worker_route_matrix_through_selector`, `test_review_route_matrix_is_codex`, `test_repeated_input_is_byte_stable`을 명시적으로 둔다.

### 중간 검증

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

기대 결과: 순수 policy와 selector-level S01~S03 테스트가 모두 PASS한다.

## 수정 파일 요약

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

## 최종 검증

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

기대 결과: S01 schema/invalid input, S02 네 KST 경계, S03 worker/review 전체 matrix가 모두 PASS한다. Python unittest cache 사용 여부는 해당 없으며 외부 provider를 호출하지 않는다.

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