# SDD: Agent Task 동적 실행 Target Selector

## 위치

- Milestone: [Milestone 문서](../../../phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md)
- Phase: [PHASE.md](../../../../phase/automation-runtime-bridge/PHASE.md)

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
  - 없음

## 문제 / 비목표

- 문제: 정적 lane/G 파일명만으로는 시간대, cloud quota, 이전 실행 실패, 재개 여부를 반영할 수 없고, dispatcher가 호출 시마다 route를 다시 계산하면 시간 경계에서 같은 work unit의 target과 selfcheck 정책이 바뀔 수 있다. 실행 대상 선택과 failover를 결정적 상태 계약으로 분리하면서도 한 branch의 실패나 user review가 독립 branch까지 중단시키지 않아야 한다.
- 비목표:
  - `finalize-task-routing`의 lane/G 의미 판정 변경
  - cloud quota 구매·credential 회전·billing 시스템 구축
  - adapter 간 native session protocol 통합
  - 알림 UI와 운영 dashboard 구현

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Milestone 문서](../../../phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md) | 정책 범위, 기능 Task와 완료 상태의 장기 원장 |
| Selector Code | `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | 구현 후 selector 입출력, policy matrix와 reason code의 원본 |
| Dispatcher State | `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`와 `.git/agent-task-dispatcher/state.json` | `task/plan/tag` 세대별 route pin, stage별 failure budget, failover와 resume의 실행 원본 |
| Quota Status | `apps/node/internal/adapters/cli/status`를 호출하는 좁은 JSON probe | Go parser를 중복 구현하지 않고 cloud `available`, `exhausted`, `unknown` snapshot을 제공함 |
| External Provider | Gemini/Claude/Codex CLI adapter가 보고한 normalized status와 runtime result | generic stderr만으로 provider/quota 상태를 추정하지 않음 |
| User Decision | `local-G07~G08`은 KST 주간 Gemini Medium·야간 Laguna와 단방향 failover/selfcheck를 canonical으로 한다 | 반복·review 횟수나 일반 실패로 cloud 승격하지 않는다. failover는 확정된 provider target 불가에만 허용한다. |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| `unselected` | PLAN `task/plan/tag` 세대에 persisted decision이 없음 | `selected` 또는 `blocked_task` | task file, worker/review stage, KST 시간 구간, quota snapshot, route rule |
| `selected` | selector가 eligible `adapter + target`을 반환하고 decision을 work unit에 저장함 | `executing` | selector JSON과 persisted decision |
| `executing` | 선택 target invocation이 시작됨 | `completed`, `selfcheck`, `recovering`, `blocked_task`, `review_blocked` | invocation result, normalized error, review 상태 |
| `recovering` | 동일 stage 실패 예산이 10 미만이고 same-target recovery가 가능함 | `executing`, `failover_selected` 또는 `blocked_task` | stage별 누적 failure count, 확정된 provider target 불가 reason, quota snapshot |
| `failover_selected` | quota/rate-limit, context/output limit, model unavailable 또는 transport disconnect가 확정되어 현재 primary target을 계속 쓸 수 없고 아직 사용하지 않은 alternate가 있음 | `executing` | 이전 decision, PLAN/locator, normalized output·raw log, workspace; 일반 실패·반복 횟수로 진입하지 않고 이전 target으로 bounce하지 않음 |
| `selfcheck` | 실제 worker 완료 target이 local model임 | `completed`, `recovering`, `blocked_task` | completing target의 pinned local target과 selfcheck stage 결과; 새 route를 평가하지 않음 |
| `review_blocked` | 해당 work unit 진행에 필요한 user review가 미해결임 | `selected` 또는 `blocked_task` | review evidence; 해결 후에도 같은 pinned decision을 사용하고 해당 task의 의존 branch만 대기 |
| `blocked_task` | 실패 예산 10회 도달, 야간 Laguna 실패 뒤 Gemini quota exhausted 또는 eligible target 부재 | terminal | persisted blocker와 실패 evidence; 독립 branch는 계속 실행 |
| `completed` | 현재 stage가 성공했고 worker인 경우 필요한 local selfcheck까지 성공함 | terminal | stage 성공 시 해당 stage failure budget 초기화, 결과 artifact와 완료 evidence |

- `blocked_task`는 현재 자동 실행의 terminal state다. 명시적 `retry_blocked`는 해당 task group의 blocker와 stage 실패 카운터만 초기화하고 route history는 보존한다. quota exhaustion으로 대기하던 단방향 failover는 새 snapshot으로 eligibility를 다시 평가할 수 있지만, 시간 변화·일반 실패·실패 횟수만으로 새 initial route, alternate 또는 cloud 승격을 만들거나 이미 사용한 이전 target으로 bounce하지 않는다.

## Interface Contract

- 계약 원문: 없음
- 입력:
  - CLI 필수 입력: `select_execution_target.py <task-file>`
  - `stage`: `worker | review`. 생략하면 task file의 `PLAN-*` / `CODE_REVIEW-*` prefix에서 판별함
  - `task_file`: 정적 routing source인 `*-{local|cloud}-GNN.md` 경로
  - `work_unit_id`: PLAN 첫머리 `task/plan/tag` 세대로 selector가 파생하는 안정적인 identity. PLAN 본문만 바뀌면 유지하고 plan 또는 tag 세대가 바뀌면 새 identity가 됨
  - `prior_decision`: dispatcher state에서 불러오는 선택 target, 사용한 candidate와 transition history; 최초 선택이면 없음
  - `quota_snapshot`: 선택 입력에서 생략할 수 있는 cloud usage normalized JSON. runtime 기본은 selector가 좁은 JSON probe를 호출하고, dispatcher의 같은 admission batch 재사용 또는 테스트에서는 snapshot id, adapter, target/profile, status, 관측 시각을 가진 값을 주입할 수 있음
  - `evaluated_at`: 기본은 runtime clock의 평가 시각. 시간 정책은 `Asia/Seoul`로 변환하며 테스트에서는 고정 시각을 주입할 수 있음
  - `transition`: `initial | resume | failover`. `resume`은 prior decision을 그대로 쓰고 `failover`만 다음 candidate를 선택함
- 출력:
  - `schema_version`, `work_unit_id`, `stage`, `lane`, `grade`
  - `selected`: canonical `adapter`, `target`, `execution_class=local_model|cloud_model`, `selfcheck_required`
  - `candidates`: `candidate_rank`가 낮을수록 먼저 시도하는 ordered 후보와 각 후보의 eligibility/rejection reason. task DAG scheduling priority와는 별개임
  - `decision`: `rule_id`, 낮을수록 먼저 적용되는 `policy_priority`, `reason_codes`, `evaluated_at`, `timezone`, `time_window`, `pinned`
  - `quota`: `snapshot_id`, `mode=bounded|unbounded`, `status=available|exhausted|unknown|not_applicable`, `source`, `checked_at`
  - `transition`: 이전 target, 다음 target, trigger, `context_transfer=logical|native_resume|none`
- 금지:
  - active/resume work unit의 persisted decision을 시간 변화만으로 교체하지 않는다.
  - generic stderr나 model list만으로 quota exhausted 또는 provider 장애를 단정하지 않는다.
  - cross-adapter 전환을 native session transfer로 기록하지 않는다.
  - local `quota_mode=unbounded`를 실패 예산 무제한 또는 동시 실행 강제 상한으로 해석하지 않는다.
  - user review/blocker가 없는 독립 task를 다른 branch의 실패 때문에 중단하지 않는다.
  - selfcheck와 resume을 새 최초 선택으로 처리하거나 공식 CODE_REVIEW를 worker route matrix로 재라우팅하지 않는다.
- quota 정규화:
  - JSON probe는 target/profile별 required cap set을 선언한다. Gemini처럼 overall과 matching model cap을 모두 제공하는 profile은 둘을 required로 사용할 수 있고, 제공하지 않는 cap을 임의로 만들어내지 않는다.
  - required cap 중 하나라도 confirmed `0% remaining`이면 `exhausted`다.
  - required cap이 모두 관측되고 모두 positive remaining이면 `available`이다.
  - checker 오류, parse 실패, required cap 누락 또는 상충하는 evidence는 `unknown`이다.
  - `unknown` cloud candidate는 각 work unit에서 1회만 시도하되 unknown 자체로 target 전역 동시 실행 cap을 만들지 않는다. runtime quota 오류가 확인되면 같은 snapshot을 `exhausted`로 갱신해 다음 candidate 또는 task-local blocker로 전환한다.
- decision 고정:
  - 최초 `initial` decision은 `task/plan/tag` 세대에 저장한다. KST 구간 변화, PLAN 본문 수정과 dispatcher 재시작은 재선택 사유가 아니다.
  - `failover`는 확정된 provider target 불가 reason이 있을 때만 ordered candidate의 아직 사용하지 않은 다음 target으로 이동한다. 시간대, quota 회복, 일반 실패 또는 실패 횟수만으로 이전 target에 bounce하거나 cloud로 승격하지 않는다.
  - `selfcheck`는 worker를 완료한 pinned local target을 그대로 사용하며 selector의 새 `initial` 호출 대상이 아니다.
  - selector는 실패 횟수를 증가시키거나 초기화하지 않는다. dispatcher가 `recovery_failures[stage]`를 소유하고 target 전환에서는 보존하며 해당 stage 성공에서만 초기화한다.
- 정책 matrix:
  - `local-G01~G06`: `pi + iop/ornith:35b`, local selfcheck
  - `local-G07~G08` 주간 `[07:00,23:00)`: `agy + Gemini 3.6 Flash (Medium)`, cloud completion이므로 selfcheck 없음
  - `local-G07~G08` 야간 `[23:00,07:00)`: `pi + iop/laguna-s`, local completion이므로 pinned Laguna selfcheck
  - `local-G09~G10`: `claude + claude-opus-4-8`, cloud completion이므로 selfcheck 없음
  - `cloud-G01~G02`: `agy + Gemini 3.6 Flash (Low)`
  - `cloud-G03~G04`: `agy + Gemini 3.6 Flash (Medium)`
  - `cloud-G05~G06`: `agy + Gemini 3.6 Flash (High)`
  - `cloud-G07~G08`: `claude + claude-opus-4-8`
  - `cloud-G09~G10`: `codex + gpt-5.6-sol` xhigh
  - 모든 공식 `CODE_REVIEW-*`: Codex `gpt-5.6-sol` xhigh 고정. worker candidate matrix와 quota 기반 재선택을 적용하지 않음
  - `local-G07~G08`은 확정된 provider target 불가에 한해 주간 Gemini→Laguna, 야간 Laguna→quota가 있는 Gemini Medium으로 한 번 logical failover하며 used target으로 bounce하지 않음
  - 그 밖의 cloud worker는 기존 promotable failure chain을 유지하되 같은 stage의 10회 예산을 공유함
  - cloud completion은 selfcheck 없음. local completion만 selfcheck를 실행함
  - target별 정책 semaphore는 두지 않으며 dependency-ready task를 모두 dispatch함

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `selector-contract` | 유효한 task file, `task/plan/tag` 세대, 고정 시각·quota·prior decision | selector를 반복 호출함 | candidate rank와 decision evidence를 포함한 JSON이 완전하며 동일 입력은 동일 결정을 반환하고 invalid 입력은 명시 오류가 됨 |
| S02 | `time-route` | `local-G07` 또는 `local-G08`과 네 KST 경계 시각 | 최초 route를 평가함 | `[07:00,23:00)`은 Gemini Medium, `[23:00,07:00)`은 Laguna가 선택됨 |
| S03 | `grade-route` | worker local/cloud `G01~G10`과 CODE_REVIEW matrix | 모든 최초 route를 평가함 | 빈 grade 없이 확정된 adapter+target에 매핑되고 cloud `G01~G06`에 Gemini가 남지 않으며 CODE_REVIEW는 Codex로 고정됨 |
| S04 | `quota-input` | target/profile required cap set, overall/model-specific remaining, parse 실패 및 local 후보 | JSON probe와 admission을 평가함 | required cap 중 0은 exhausted, 모두 양수는 available, 누락·충돌은 unknown이고 unknown은 work unit별 1회만 시도되되 target 전역 cap은 생기지 않으며 local은 `unbounded/not_applicable`로 반환됨 |
| S05 | `route-pin` | 주간에 선택된 `task/plan/tag` 세대가 PLAN 본문 변경, 야간 경계와 dispatcher 재시작을 지남 | resume함 | 시간·본문 변경만으로 target이 바뀌지 않고 plan/tag 세대가 바뀔 때만 새 initial decision이 생성됨 |
| S06 | `context-failover` | 주간 Gemini 또는 야간 Laguna의 확정된 provider target 불가 | failover를 평가함 | 주간은 Laguna로, 야간은 quota가 있는 Gemini로 한 번만 이어지고 일반 실패·반복 횟수만으로는 전환하지 않으며 quota가 없으면 해당 task만 차단되고 이전 target으로 bounce하지 않음 |
| S07 | `failure-budget` | 같은 work unit의 동일 stage에서 target 전환을 포함한 연속 실패 | 자동 복구를 반복함 | stage budget이 target 간 공유되고 다른 stage와는 분리되며 실패 횟수 자체는 target 전환·cloud 승격 근거가 아니고 stage 성공 시 초기화됨 |
| S08 | `dependency-drain` | 병렬 DAG의 한 branch가 user review 또는 terminal blocker에 걸림 | scheduler가 drain함 | 해당 branch와 의존 task만 대기/차단되고 다른 dependency-ready branch는 완료됨 |
| S09 | `dispatch-integration` | selector decision이 저장된 worker/review initial, resume와 failover 작업 | dispatcher가 invocation함 | 실행 target, `task/plan/tag` persisted decision 및 transition history가 일치함 |
| S10 | `selfcheck-policy` | Gemini→Laguna 완료, Laguna→Gemini 완료, cloud lane 완료 | completion stage를 처리함 | 첫 경우만 pinned Laguna로 selfcheck가 실행되고 나머지는 selfcheck 없이 완료됨 |
| S11 | `throughput-policy` | 동일 target을 쓰는 독립 dependency-ready task 여러 개와 같은 admission batch quota 조회 | dispatch함 | target별 정적 cap 없이 admission되며 같은 snapshot id가 batch에서 재사용되고 조회 실패는 `unknown`으로 격리됨 |
| S12 | `audit-tests` | 성공, failover, quota block, budget block, review block 실행 | evidence를 수집함 | work unit, rule, candidate, reason, transition이 테스트·로그·운영 설명에서 추적됨 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | selector schema/invalid input 단위 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: selector-contract`, 테스트 결과 |
| S02 | KST 네 경계값 단위 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: time-route`, 경계 matrix 결과 |
| S03 | worker local/cloud G01~G10 및 CODE_REVIEW route matrix 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: grade-route`, matrix snapshot |
| S04 | Go usage checker JSON bridge, required-cap tri-state, unknown 1회와 local unbounded 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: quota-input`, quota snapshot evidence |
| S05 | persisted route resume/restart 통합 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: route-pin`, decision state evidence |
| S06 | 주간·야간 failover와 logical context 통합 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: context-failover`, transition evidence |
| S07 | 동일 stage target 전환, stage 분리와 성공 초기화를 포함한 10회 failure budget 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: failure-budget`, stage counter evidence |
| S08 | user review/blocker 병렬 DAG drain 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: dependency-drain`, 독립 branch 완료 evidence |
| S09 | dispatcher worker/review initial, resume와 failover 통합 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: dispatch-integration`, invocation audit |
| S10 | completing target 기반 selfcheck 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: selfcheck-policy`, stage evidence |
| S11 | 동시 dispatch와 batch quota snapshot 테스트 | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: throughput-policy`, admission evidence |
| S12 | 회귀 테스트, reason/transition 로그, 운영 문서 diff | `agent-task/m-agent-task-runtime-target-selector/...` | `Roadmap Completion: audit-tests`, 최종 test summary |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 2026-07-25: KST 주간/야간 구간, target matrix, Medium reasoning, failover, quota, local selfcheck, 10회 실패 예산, work-unit pin, task-local blocker와 무제한 target별 처리량 정책을 대화에서 확정했다.
- 2026-07-25: 기존 target matrix는 유지하되, 반복·review 횟수·일반 실패로 cloud 승격하지 않고 failover는 확정된 provider target 불가에만 허용하도록 보정했다.
- 2026-07-26: `local-G07~G08` canonical 정책을 KST 주간 Gemini Medium ↔ 야간 Laguna로 확정했다. Gemini 단일 target/no-Laguna 서술과 완료 표시는 유효하지 않으며 S02/S06/S10 및 Milestone `time-route`/`grade-route`에 맞춰 재구현·재검증한다.

## 작업 컨텍스트

- 표준선: selector는 `adapter + target`을 결정적으로 반환하고, skill은 호출 시점과 persisted decision 소비 절차를 정의한다. static lane/G 분류는 기존 `finalize-task-routing` 책임으로, 공식 review는 기존 Codex 고정 lifecycle로 남긴다.
- 후속 SDD: 없음
