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

# Plan - Admission batch quota 재사용과 target cap 제거

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

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

## 배경

dispatcher는 ready task를 넓게 고르지만 target별 semaphore가 실제 동시 실행을 다시 제한한다. 또한 같은 scheduling admission batch의 selector 결정들이 하나의 quota snapshot identity를 공유한다는 보장이 없어 관측 drift가 생길 수 있다.

## 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:
  - `throughput-policy`: target별 정적 cap 없이 ready task dispatch와 batch quota snapshot 재사용
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `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/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_select_execution_target.py`
- `apps/node/internal/adapters/cli/status/quota.go`
- `apps/node/cmd/node/quota_probe.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: S11 → `throughput-policy`.
- Evidence Map S11 행의 동시 dispatch와 batch quota snapshot evidence를 따라 동일 target 무제한 admission, 같은 key의 snapshot id/관측 시각 재사용, probe failure의 batch-local unknown 격리를 구현 체크리스트와 집중 테스트에 반영했다.

### 테스트 환경 규칙

- `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 유지보수는 필요하지 않다.
- quota probe와 runner는 fake로 주입해 호출 횟수, batch identity, 실제 동시 시작을 결정적으로 측정한다.
- 계획 시점 전체 Python suite 기준선은 169 tests PASS다.

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

- `dispatch.py`의 target semaphore map은 Laguna 2, Ornith 3, AGY 1 등의 정적 cap을 강제한다.
- 같은 target의 ready task 수가 cap을 넘을 때 모두 admission되는지 검증하지 않는다.
- 같은 scheduling batch의 여러 selector 결정이 mixed target에서도 동일 batch `snapshot_id`/`checked_at`과 key별 target evidence를 쓰는지 검증하지 않는다.
- probe 실패 `unknown`이 persisted work unit을 재선택하거나 target 전역 cap으로 변질되지 않는지 검증하지 않는다.

### 심볼 참조

- 제거 대상은 `run_escalating(invocation_semaphore)`(`dispatch.py:2696,2801-2821`), `run_worker(semaphores)`(`dispatch.py:3420,3441`), `run_selfcheck(semaphores)`(`dispatch.py:3465,3509`), map 생성(`dispatch.py:3755-3761`), 호출부(`dispatch.py:4291,4305`)다.
- 테스트 signature/semaphore fixture `test_dispatch.py:2512,2694,2831,2890,3066,3139,4981,4996`를 새 호출 계약으로 갱신한다.
- Node quota JSON producer는 소비 계약만 확인하고 변경하지 않는다.

### 분할 판단

- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`이고 topology는 `03 → 04 → 05 → 06 → 07 → 08`이다.
- 현재 `07+06_throughput_policy`의 predecessor `06+05_selfcheck_policy`는 active plan만 있고 `complete.log`가 없어 미충족이다. `06`이 `05`를 선행하므로 live selector 통합도 전이적으로 충족한다.
- selfcheck와 정책상 분리되지만 동일 write-set 충돌을 피하려고 직렬화했고 최종 audit는 `08`이 소유한다.

### 범위 결정 근거

- dependency readiness, task-local blocker, 전체 process resource safety 장치는 유지한다.
- 모델/target별 인위적 정책 cap만 제거하며 OS나 subprocess 자체의 안전 제어를 새로 설계하지 않는다.
- quota batch envelope은 scheduling admission batch 수명으로만 재사용하고 영속/global cache를 만들지 않는다. persisted work-unit decision과 derived quota evidence는 별도다.
- `agent-spec/index.md`와 `agent-contract/index.md`에 매칭되는 활성 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=`boundary_contract`, `concurrent_consistency`; 이 기록은 lane/G를 바꾸지 않는다.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/2/1/1/2`, 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)=`1/2/1/1/2`, final=`cloud-G07`, filename=`CODE_REVIEW-cloud-G07.md`.

## 구현 체크리스트

- [ ] target별 정적 semaphore와 concurrency map이 live dispatch 경로에서 제거된다.
- [ ] 한 scheduling admission batch가 unique adapter+target+profile별 probe를 한 번씩 실행해 aggregate envelope을 만들고 모든 최초 selector 결정이 동일 batch snapshot id와 checked_at을 재사용한다.
- [ ] 서로 독립적인 동일 target ready task가 target cap 없이 모두 admission된다.
- [ ] quota probe 실패는 해당 batch/key의 unknown으로 격리되고 persisted work unit은 재선택하지 않으며 새 generation·명시적 failover/retry만 새 snapshot을 관측한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REFACTOR-1] Target별 semaphore 제거

- 문제: `dispatch.py:2696,2801-2821,3420-3509,3755-3761,4291-4305`의 target concurrency map과 invocation semaphore가 ready candidate를 고른 뒤 adapter/target 이름에 따라 병렬도를 제한한다.
- 해결 방법:

```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:2811
async with invocation_semaphore:
    return await invoke(...)
```

```python
# After
return await invoke(...)
```

  target 정책 cap과 관련 생성/전달 인자를 제거한다. dependency, task lifecycle, work-log lock처럼 정합성에 필요한 lock은 보존한다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: semaphore map, parameter, acquisition 제거.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: 동일 target 동시 admission 회귀.
- 테스트 작성: `test_dispatch.py`의 `ThroughputQuotaBatchTest`에서 기존 cap보다 많은 독립 task가 `asyncio.wait_for`로 제한된 시간 안에 runner barrier에 모두 도달한 뒤 release되는지 검증하고 timeout 자체를 failure evidence로 만든다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — bounded concurrency test가 hang 없이 PASS해야 한다.

### [REFACTOR-2] Admission batch quota snapshot

- 문제: `dispatch.py:4228`의 ready admission pass에서 task별 selector 평가가 single-target Go probe를 개별 호출하면 같은 pass 안에서도 quota 상태/시각이 달라지고, 한 snapshot을 mixed target에 재사용하면 unmatched target이 `unknown`이 된다.
- 해결 방법:

```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4228
for task, stage in select_dispatch_candidates(ready)[0]:
    resolve_decision(task)
```

```python
# After
batch_snapshot = quota_provider.aggregate(
    snapshot_id=batch_id,
    checked_at=admission_time,
    keys=unique_probe_keys,
)
for task in ready_tasks:
    resolve_decision(task, quota_snapshot=batch_snapshot)
```

  probe key는 `(adapter, canonical target, command/profile, required_caps)`다. 한 pass에서 key별 Go probe를 최대 한 번 실행하고 결과 targets를 immutable aggregate envelope으로 합치며 top-level batch id/time을 모든 최초 decision에 전달한다. child Go snapshot id/reason은 target entry evidence로 보존한다. 동일 target N개는 probe 1회, mixed target은 unique key 수만큼 호출한다. local decision은 `not_applicable`이고 persisted decision이 있는 resume/selfcheck는 admission probe 대상이 아니다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: batch context와 snapshot lifetime.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: probe count와 identity assertions.
- 테스트 작성: `ThroughputQuotaBatchTest`에서 동일 target N개=probe 1회, mixed target=unique key별 1회, 전체 decision의 같은 batch id/time, target별 status 매칭, local/resume probe 0회를 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — probe count/envelope identity matrix가 PASS해야 한다.

### [REFACTOR-3] Unknown 격리와 회귀

- 문제: `dispatch.py:4228` admission에서 probe exception/parse failure 또는 runtime quota failure를 shared envelope에 in-place 반영하면 독립 work unit의 quota evidence와 route pin을 오염시킬 수 있다.
- 해결 방법:

```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4228
candidates, deferred, phase_wait_reason = select_dispatch_candidates(ready)
```

```python
# After
derived = derive_work_unit_quota_evidence(
    decision,
    status="exhausted",
    reason="confirmed_runtime_provider_quota",
)
```

  probe failure는 해당 batch/key의 `unknown` entry로 정규화하고 work unit별 1회 admission은 selector에 맡긴다. batch envelope은 immutable로 폐기하며 같은 persisted work unit은 다음 pass에서 재probe/재선택하지 않는다. confirmed runtime `provider-quota`는 shared envelope을 바꾸지 않고 해당 work unit decision에서 같은 observation identity를 계승한 derived `exhausted` evidence로만 교체한다. 새 plan/tag generation, 아직 사용하지 않은 failover eligibility, 명시적 retry-blocked만 새 snapshot을 얻는다.
- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: batch-local error normalization과 disposal.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: failure/recovery batch 회귀.
- 테스트 작성: `ThroughputQuotaBatchTest`에서 첫 batch key 하나만 unknown, 같은 work unit resume은 probe 0회/pin 유지, 새 generation의 다음 batch available 회복, confirmed provider-quota의 task-local derived exhausted, generic stderr의 unknown 유지를 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v` — isolation/pin/recovery case가 PASS해야 한다.

## 의존 관계 및 구현 순서

1. `06+05_selfcheck_policy`의 archived `complete.log`가 PASS여야 한다. 계획 작성 시점에는 아직 미충족이다.
2. REFACTOR-2로 batch 경계를 만든 뒤 REFACTOR-1의 cap 제거와 REFACTOR-3의 error 격리를 검증한다.
3. `06`과 `07`이 모두 PASS해야 `08+06,07_audit_closure`를 시작한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-1, REFACTOR-2, REFACTOR-3 |
| `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 ThroughputQuotaBatchTest -v` — concurrency/envelope/isolation 회귀 PASS.
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v` — 독립 branch drain 회귀 PASS.
- `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite PASS; cache output은 허용하지 않는다.
- `python3 -m py_compile agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` — exit 0.
- `git diff --check` — 출력 없음.

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