<!-- task=m-agent-task-runtime-target-selector/03+01,02_route_pin_state plan=0 tag=REFACTOR -->

# Plan - Selector decision 영속 상태 기반

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

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

## 배경

selector는 이미 `task/plan/tag` identity와 `initial`/`resume` 결정을 제공하지만 dispatcher는 정적 `route_agent()`만 사용한다. 이 작업은 broad invocation 전환에 앞서 selector decision을 dispatcher state에 안전하게 보존하고 다시 읽는 내부 기반을 만든다.

## Archive Evidence Snapshot

- `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/01_selector_contract/complete.log`: 최종 PASS, 잔여 Required/Suggested/Nit 없음. selector schema·KST 경계·worker/review matrix를 변경한 selector/policy와 테스트가 31-test suite 및 집중 회귀 4건에서 PASS했다.
- `agent-task/archive/2026/07/m-agent-task-runtime-target-selector/02+01_quota_input/complete.log`: 최종 PASS, 잔여 Required/Suggested/Nit 없음. Go quota probe/normalizer와 selector admission 변경이 Go·Python·gofmt·diff 검증에서 PASS했다.
- Roadmap carryover: `selector-contract`, `time-route`, `grade-route`, `quota-input`은 완료됐고 이 foundation task는 새 Roadmap Target 완료를 주장하지 않는다.
- 이 작업은 두 `complete.log`에 기록된 selector JSON과 quota snapshot 계약만 소비한다. 추가 세부가 필요할 때도 위 두 경로만 읽고 archive plan/review를 탐색하지 않는다.

## 분석 결과

### 읽은 파일

- `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/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`
- `apps/node/internal/adapters/cli/status/quota.go`
- `apps/node/internal/adapters/cli/status/quota_test.go`
- `apps/node/cmd/node/quota_probe.go`
- `apps/node/cmd/node/quota_probe_test.go`
- `apps/node/cmd/node/main.go`
- `agent-test/local/rules.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-contract/index.md`

### SDD 기준

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- 대상 Acceptance Scenario: S05 → Milestone Task `route-pin`.
- Evidence Map S05 행은 persisted route resume/restart 통합 테스트와 decision state evidence를 요구한다. 이에 따라 세대별 저장·복원, 시간/본문 변경 pin, restart와 malformed-state 검증을 구현 체크리스트와 최종 검증에 넣었다.
- 이 subtask는 S05의 저장·복원 기반만 구현하므로 `Roadmap Targets`를 두지 않는다. 실제 invocation까지 연결해 S05를 닫는 소유자는 `05+04_dispatch_integration`이다.

### 테스트 환경 규칙

- `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, Docker, 원격 runner는 사용하지 않는다. 기준선은 전체 Python suite 169 tests PASS다.

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

- selector 자체 resume pin은 `test_resume_pins_prior_target_across_time`이 검증한다.
- dispatcher state에는 selector decision 필드가 없고 reopen/body-change/new-generation 회귀가 없다.
- malformed persisted decision을 fail-closed로 처리하는 dispatcher 테스트가 없다.

### 심볼 참조

- rename/remove 없음.
- 신규 decision bridge의 소비자는 이 subtask에서는 `StateStore.task_state()`(`dispatch.py:473`), `peek_task_state()`(`dispatch.py:494`), `update_task()`(`dispatch.py:512`)와 집중 테스트로 제한한다.

### 분할 판단

- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`다.
- 기존 완료: `01_selector_contract`, `02+01_quota_input`.
- 현재: `03+01,02_route_pin_state`; predecessor `01`, `02`는 위 archive `complete.log`로 충족한다.
- 후속: `04+03_failover_budget`, `05+04_dispatch_integration`, `06+05_selfcheck_policy`, `07+06_throughput_policy`, `08+06,07_audit_closure`.
- 상태 schema와 broad call-site rollout은 독립 review가 필요하므로 분리한다.

### 범위 결정 근거

- failover target 선택, invocation 전환, selfcheck, semaphore 제거, skill 문서 갱신은 후속 subtask로 제외한다.
- Node quota producer는 변경하지 않고 기존 JSON 계약만 소비한다.
- `agent-spec/index.md`와 `agent-contract/index.md`에 Agent Task selector/dispatcher와 매칭되는 활성 spec/contract 문서가 없다. 새 문서를 추정하지 않고 승인 SDD, 코드와 테스트를 기준으로 삼는다.

### 최종 라우팅

- 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/1`, final=`local-G07`, filename=`PLAN-local-G07.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/1/1`, final=`cloud-G07`, filename=`CODE_REVIEW-cloud-G07.md`.

## 구현 체크리스트

- [ ] dispatcher가 selector를 단일 내부 bridge로 호출하고 JSON decision을 canonical `AgentSpec`으로 변환한다.
- [ ] `StateStore`가 stage별 selector decision과 transition history를 `task/plan/tag` 세대에 보존하고 task별 malformed/stale state를 해당 task blocker로 fail-closed 처리한다.
- [ ] reopen, 시간 경계, PLAN 본문 변경, 새 plan/tag 세대, malformed state와 독립 branch drain 회귀 테스트를 추가하고 집중·전체 검증을 통과한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REFACTOR-1] Selector bridge와 canonical AgentSpec

- 문제: `dispatch.py:977`의 `route_agent()`는 정적 lane/G matrix를 다시 구현하며 selector JSON을 소비하지 않는다.
- 해결 방법:

```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:977
def route_agent(task: Task) -> AgentSpec:
    if task.lane is None or task.grade is None:
        raise RuntimeError("PLAN route를 판별할 수 없다")
```

```python
# After
def evaluate_execution_decision(task, *, stage, transition, prior, quota, evaluated_at):
    ...

def agent_spec_from_decision(decision):
    ...
```

  sibling selector module을 path-stable하게 load한다. selector의 canonical target은 persisted decision에 그대로 보존하되 `AgentSpec` 변환 시 Pi target `iop/ornith:35b`를 검증한 뒤 CLI `--model`용 `ornith:35b`로 변환하고 다른 adapter target은 그대로 쓴다. `execution_class`와 `selfcheck_required`는 decision에서 손실 없이 유지한다. task별 invalid/malformed/no-eligible 입력은 그 task의 `blocked` reason으로 보존하고 독립 task는 drain하며, 전체 `state.json` 자체가 읽히지 않을 때만 dispatcher global terminal로 둔다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: selector loader, decision evaluator, conversion/validation helper.
- 테스트 작성: `test_dispatch.py`의 `RouteDecisionPersistenceTest`에 worker/review canonical target, Pi prefix 변환, task-local malformed/no-eligible blocker와 독립 ready branch fixture를 추가한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py RouteDecisionPersistenceTest -v`가 PASS해야 한다.

### [REFACTOR-2] 세대별 persisted decision 상태

- 문제: `dispatch.py:473-511`의 task state는 worker 완료와 failure count만 저장하며 route decision/history가 없다.
- 해결 방법:

```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:473
current = {
    "plan_hash": task.plan_hash,
    "worker_done": False,
    "recovery_failures": {},
}
```

```python
# After
current = {
    ...,
    "execution_decisions": {},
    "route_transition_history": [],
}
```

  `plan_hash=plan_identity()` 세대를 유지해 본문 수정은 같은 state를 쓰고 plan/tag 변경만 초기화한다. persisted JSON은 selector validator로 재검증하며 손상된 decision을 자동 재선택하지 않고 해당 task만 차단한다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: 기본/peek/update state와 decision 저장·복원 helper.
- 테스트 작성: `test_dispatch.py`의 `RouteDecisionPersistenceTest`에서 temp `state.json`으로 reopen, day→night resume, body-only edit, generation reset, corrupt nested JSON과 독립 branch 지속을 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py RouteDecisionPersistenceTest -v`가 PASS해야 한다.

### [REFACTOR-3] Decision state 회귀 고정

- 문제: `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py:1-5088`의 기존 169-test 기준선은 dispatcher persisted selector state와 task-local malformed blocker를 실행하지 않는다.
- 해결 방법: temp workspace의 실제 `state.json` reopen을 사용하고 selector clock/quota를 주입해 byte-stable decision과 reset 경계를 검증한다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: `RouteDecisionPersistenceTest`.
- 테스트 작성: 위 5개 경로를 모두 새 테스트로 작성한다.
- 중간 검증: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*execution_target*.py'`가 PASS해야 한다.

## 의존 관계 및 구현 순서

1. `01_selector_contract`와 `02+01_quota_input`은 archive `complete.log`로 충족됐다.
2. REFACTOR-1 → REFACTOR-2 → REFACTOR-3 순서로 구현한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-1, REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-1, REFACTOR-2, REFACTOR-3 |

## 최종 검증

- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py RouteDecisionPersistenceTest -v` — 새 state 회귀 PASS.
- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — 전체 suite PASS; cache를 사용하지 않는다.
- `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`의 구현 에이전트 소유 섹션을 채운다.
