finalize task routing & orchestrate improvements, roadmap/SDD updates

This commit is contained in:
toki 2026-07-26 00:58:29 +09:00
parent 9d2c039179
commit 51e1a20ff4
31 changed files with 3754 additions and 723 deletions

View file

@ -2,6 +2,7 @@
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
- 이 workspace에서는 전용 `apply_patch` 도구가 `/config/workspace/iop`를 읽지 못한다. 이를 호출하거나 재시도하지 말고 `git apply`로 최소 unified diff를 적용한 뒤 `git diff --check`로 확인한다.
- 기존 구조를 우선한다. 새 파일 생성보다 기존 파일 수정을 우선한다.
- `agent-ops/rules/common/**``agent-ops/skills/common/**`은 중앙 관리되는 공통 영역이므로 어떤 프로젝트 작업에서도 사용자가 직접 지시하지 않는 이상 절대 직접 수정하지 않는다. 프로젝트별 규칙과 스킬은 반드시 대응하는 `project/**` 영역에만 반영한다.
- 최종 답변은 한국어로 한다.

View file

@ -1,174 +1,127 @@
---
name: finalize-task-routing
description: PLAN/CODE_REVIEW 작성 직전 현재 작업 증거만으로 cloud/local lane과 G01-G10 등급을 독립 평가하고, 구현의 장기 반복 위험과 공식 review cloud-G09 capability floor를 적용해 canonical 파일명을 확정한다. 최초 plan, review 실패 후 follow-up plan, 라우팅 재평가, lane/G 판단, routed filename 결정에 사용한다.
description: PLAN/CODE_REVIEW 작성 직전 build는 local-first로 라우팅하고 review는 실제 G와 고정 공식 리뷰 target을 확정한다. capability-gap 기반 cloud 선택과 follow-up 재평가에 사용한다.
---
# Finalize Task Routing
## 목적
계획 분석이 끝난 뒤 구현과 리뷰 target의 lane과 G 등급을 처음부터 평가하고 canonical 파일명을 반환한다.
구현 target은 현재 작업에 이미 드러난 복합 정확성 위험을 함께 판정해, 반복 루프가 시작되기 전에 필요한 cloud lane과 capability floor를 적용한다.
이 스킬은 라우팅 판단의 단일 원본이며 plan/review 내용을 작성하거나 task 파일을 직접 생성하지 않는다.
build의 lane/G와 review의 G를 평가해 canonical 파일명을 확정한다.
공식 review는 항상 cloud의 Codex `gpt-5.6-sol` xhigh다. 이 스킬은 task 파일을 수정하지 않는다.
## 언제 호출할지
## 호출 시점
- `plan`이 최초 PLAN/CODE_REVIEW pair를 쓰기 직전
- `code-review`의 WARN/FAIL 후 `plan`이 follow-up PLAN/CODE_REVIEW pair를 다시 쓰기 직전
- 기존 lane/G 판정이 검증 실패, 증거 충돌, 범위 변화, 소유권 변화로 무효화된 뒤 재평가할 때
- 사용자가 현재 작업의 cloud/local, G 등급, canonical task 파일명만 요청할 때
- `plan`이 최초 또는 follow-up pair를 쓰기 직전
- scope, 증거, 소유권, 결정 조건이 달라져 기존 route가 무효가 된 때
- 사용자가 현재 pair의 lane/G 판정을 요청한 때
## 입력
- `targets`: `build`, `review`, 또는 둘 다. `plan`은 항상 둘 다 전달한다 (필수)
- `evaluation_mode`: 이전 라우팅이 없는 최초 평가는 `first-pass`, 무효화된 기존 라우팅이 있는 평가는 `isolated-reassessment` (필수)
- `task_snapshot`: 사용자 요구, 현재 scope, 수정/영향 경로, 불변조건, 상태 전이, 검증 계획과 실제 증거 (필수)
- `context_snapshot`: 함께 유지해야 하는 source/test/diff/log/contract 범위와 분할 가능성 (필수)
- `ownership_snapshot`: 공유 상태, 병행 작업, sibling task, 외부 책임 경계 (필수)
- `decision_snapshot`: 미해결 의미 판단, 사용자 권한이 필요한 결정, 외부 환경 의존성 (필수)
- `loop_risk_snapshot`: `build`가 target일 때 아래 다섯 범주의 현재 작업 원시 사실 (필수)
- `ordered_transitions`: `state_count`, `adverse_paths`(partial/retry/cancel/recovery/stale/duplicate), `evidence`
- `concurrent_consistency`: `actor_count`, `constraints`(atomic snapshot/lock order/exactly-once/race-free), `evidence`
- `boundary_contract`: `component_count`, `consumer_count`, `constraints`(validation/immutability/compatibility), `evidence`
- `structured_interpretation`: `mechanisms`(parser/lexer/protocol assembly/multi-source merge), `hazards`(nesting/quoting/precedence/deduplication), `evidence`
- `evaluation_mode`: `first-pass | isolated-reassessment`
- `targets.build`, `targets.review`: 각 target의 요구, scope, 영향 경로, 불변조건, 검증 계획과 실제 증거
- target별 `context_snapshot`: 함께 유지해야 할 source/test/diff/log/contract와 분할 가능성
- target별 `ownership_snapshot`: 공유 상태, 병행 작업, sibling task, 외부 책임
- target별 `decision_snapshot`: 미해결 의미 판단, 사용자 권한, 외부 환경 의존성
- build의 `loop_risk_snapshot`:
- `ordered_transitions`: `state_count`, `adverse_paths`, `evidence`
- `concurrent_consistency`: `actor_count`, `constraints`, `evidence`
- `boundary_contract`: `component_count`, `consumer_count`, `constraints`, `evidence`
- `structured_interpretation`: `mechanisms`, `hazards`, `evidence`
- `variant_product`: `independent_axis_count`, `combination_verification_required`, `evidence`
- `invalidation_evidence`: 검증 실패, 실제 로그, review finding, 변경된 범위 등 현재 판정을 바꾸는 원시 증거 (선택)
- `invalidation_evidence`: 실패 로그, review finding, 변경된 범위 등 재평가 근거
이전 `lane`, `grade`, 점수, 라우팅 사유, routed filename은 입력에서 제외한다.
이전 PLAN/CODE_REVIEW 문서의 코드 경로, 실제 로그, finding은 원본에서 재확인한 뒤 원시 증거로만 사용할 수 있다.
해당 범주가 없으면 빈 목록 또는 0/false로 명시한다. 현재 증거로 부재와 미확인을 구분할 수 없으면 값을 추측하지 않고 `unknown`으로 둔다. 위험 등급이나 route 추정치를 입력하지 않는다.
이전 lane/G, 점수, 라우팅 사유와 filename은 입력에서 제외한다. 이전 로그와 finding은 원본에서 확인한 사실만 사용한다. 확인된 부재는 0/false/빈 목록, 미확인은 `unknown`이다.
## 먼저 확인할 것
## 절차
- [ ] `agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh`와 내부 formatter `finalize-task-route.sh`가 실행 가능한가
- [ ] 호출자가 plan 분석, scope, split, 검증 계획을 모두 확정했는가
- [ ] `build` target의 `loop_risk_snapshot`이 분할 이후 현재 범위의 원시 사실로 채워졌는가
- [ ] `targets`마다 필요한 구현 또는 리뷰 작업이 구분되어 있는가
- [ ] 이전 lane/G와 그 평가 문구가 입력에서 격리되었는가
- [ ] `isolated-reassessment`이면 이전 route-bearing 항목을 제거한 중립 snapshot을 현재 invocation의 sealed scratch block에 고정했는가
- [ ] 주장이나 요약이 아니라 현재 파일, diff, 명령 출력, 확정된 제약을 우선 증거로 사용했는가
### 1. 평가 격리
## 실행 절차
각 target을 `unrouted`로 시작한다. `isolated-reassessment`는 route 정보가 제거된 현재 snapshot만 평가한다. 중립 snapshot을 만들 수 없으면 `blocked: isolated-routing-unavailable`을 반환한다.
1. **평가 상태 초기화**
- 호출할 때마다 각 target을 `unrouted`로 시작한다.
- `isolated-reassessment`는 이전 route-bearing 항목을 제거한 중립 snapshot을 sealed scratch block의 유일한 평가 입력으로 고정하고, 현재 invocation 안에서 처음부터 평가한다.
- 이 격리는 입력 격리 계약이다. 새 sub-agent, delegated session, collaboration tool, background evaluator, 메시지 전달, `wait` loop를 생성하거나 요구하지 않는다.
- 중립 snapshot을 route-free 상태로 만들 수 없으면 `status: blocked`, `blocked_reason: isolated-routing-unavailable`을 반환한다.
- 이전 lane/G를 승계하거나 하한으로 사용하지 않는다.
- 실패 횟수, Required 개수, 이전 G 등급만으로 cloud 또는 상위 G를 선택하지 않는다.
- 실패는 자동 승격 조건이 아니라 현재 증거에 추가되는 판정 무효화 사건으로만 취급한다.
- 새 결과가 확정되기 전에는 이전 평가와 비교하지 않는다. 필요하면 호출자가 routed 출력 이후에만 감사용 delta를 기록한다.
실패·review 횟수, Required/finding 개수와 이전 grade는 승격 조건이나 점수가 아니다.
2. **폐쇄성 평가**
- 각 target을 bounded local execution으로 완료·판정할 수 있는지를 기준으로 아래 값을 `true`, `false`, `unknown` 중 하나로 독립 판정하고 근거를 한 줄로 기록한다.
- `scope_closed`: 정확성 조건과 영향 경로가 확정된 범위 안에서 설명되는가
- `context_closed`: 중요한 증거를 생략하지 않고 필요한 컨텍스트를 함께 다룰 수 있는가
- 계획 분석 결과, 해당 target을 수행하는 동안 긴 컨텍스트를 유지해야 한다고 판단되면 `context_closed=false`로 판정한다.
- `verification_closed`: 결정적인 명령과 관찰로 성공 여부를 증명할 수 있는가
- `evidence_trusted`: 검증이 의도한 실제 경로를 실행하며 출력이 재현 가능하고 현재 상태와 일치하는가
- `ownership_closed`: 공유 상태, 병행 작업, 외부 소유권이 판단을 모호하게 만들지 않는가
- `decision_closed`: 사용자 권한이나 미해결 외부 의미 결정 없이 작업을 완료할 수 있는가
- 동시성, dirty worktree, 외부 명령, 많은 finding은 그 자체로 기본 lane을 결정하지 않는다. 위 폐쇄성에 미치는 영향을 평가하고, build의 복합 동시성 signature 여부는 Step 6에서 별도로 판정한다.
### 2. 증거 gate
3. **판정 가능성 확인**
- 하나라도 `unknown`이면 `status: needs_evidence`와 필요한 증거를 반환한다. lane, grade, filename은 반환하지 않는다.
- `build` target의 `loop_risk_snapshot`에 필수 field가 없거나 `unknown`이면 동일하게 `needs_evidence`를 반환한다. 단순 미기재를 위험 없음으로 간주하지 않는다.
- 하나 이상의 값이 `false`이고 cloud의 더 넓은 컨텍스트나 진단도 그 폐쇄 실패를 해소할 수 없으면 `status: blocked`, 폐쇄 실패별 `blocked_reason`, 해제 조건을 반환한다. 사용자 권한, secret, 접근 권한, 제품 결정은 대표적인 예일 뿐이며 이에 한정하지 않는다.
- 호출자는 `needs_evidence`를 cloud로 해석하지 말고 증거를 보강한 뒤 이 스킬을 처음부터 다시 실행한다.
build와 review에 다음 값을 독립적으로 `true | false | unknown`으로 판정하고 파일·diff·명령 출력 근거를 한 줄씩 기록한다.
4. **기본 Lane 결정**
- 모든 폐쇄성이 `true`인 target의 `base_lane``local`로 정한다.
- `unknown`이 없고 하나 이상의 폐쇄성이 `false`이며 cloud의 더 넓은 컨텍스트나 진단이 그 실패를 실제로 해소할 수 있을 때 `base_lane``cloud`로 정한다.
- `false`를 cloud가 해소할 수 있는지 근거를 기록한다. 해소 가능성을 설명할 수 없으면 추측으로 cloud를 선택하지 않고 Step 3의 `blocked`를 반환한다.
- 실패 후에도 현재 폐쇄성이 모두 `true`이면 `base_lane: local`을 선택할 수 있다. 최종 lane은 Step 6의 현재 구조 위험 판정을 적용한 뒤 확정한다.
- 기본 lane은 capability fit이며 작업량이나 모델 사용량 균형을 위한 값이 아니다.
- `scope_closed`: 정확성 조건과 영향 경로가 확정됐는가
- `context_closed`: 중요한 증거를 빠뜨리지 않고 함께 다룰 수 있는가
- `verification_closed`: 결정적 명령과 관찰로 성공을 판정할 수 있는가
- `evidence_trusted`: 검증이 실제 경로를 실행하며 재현 가능한가
- `ownership_closed`: 공유 상태와 외부 소유권이 명확한가
- `decision_closed`: 사용자 결정이나 외부 의미 판단 없이 완료할 수 있는가
5. **기본 G 등급 재산정**
- 기본 lane을 정한 뒤에만 각 target을 아래 다섯 차원에서 `0`, `1`, `2`로 처음부터 채점한다.
- `scope_coupling`: `0` 단일 국소 변경 / `1` 여러 연결 지점 / `2` 다중 모듈·계약
- `state_concurrency`: `0` 무상태 / `1` 상태 전이 / `2` 공유 상태·순서·동시성
- `blast_irreversibility`: `0` 내부 가역 / `1` 다중 호출자·설정·지속 상태 / `2` 외부 계약·권한·비가역 영향
- `evidence_diagnosis`: `0` 직접 증거 / `1` 복수 증거 조합 / `2` 충돌·불신 증거 진단
- `verification_complexity`: `0` 단일 결정 명령 / `1` 복수 단계·통합 검증 / `2` 외부·상호작용·장시간 검증
- `base_grade``max(1, 다섯 점수의 합)`이며 유효 범위는 `G01`부터 `G10`이다.
- 이전 grade, 실패 횟수, lane 변경은 점수에 더하지 않는다.
파일 수, 긴 문서, 동시성, dirty worktree와 finding 수만으로 `false`를 주지 않는다.
6. **구현 장기 반복 위험 사전 판정**
- 이 단계의 signature 판정은 `build`에만 적용한다. `review`는 자체 폐쇄성과 점수를 먼저 독립 평가한 뒤 Step 7의 capability floor만 적용한다.
- 아래 조건을 만족하는 범주마다 최대 한 개의 signature를 기록한다.
- `temporal_state`: 순서가 있는 상태가 3개 이상이고 partial/retry/cancel/recovery/stale/duplicate 경로가 하나 이상이다.
- `concurrent_consistency`: actor/reader/writer가 2개 이상이고 atomic snapshot/lock order/exactly-once/race-free 제약이 하나 이상이다.
- `boundary_contract`: 하나의 동작 계약이나 불변조건이 3개 이상 component 또는 2개 이상 독립 consumer에 걸치고 validation/immutability/compatibility 제약이 하나 이상이다.
- `structured_interpretation`: parser/lexer/protocol assembly/multi-source merge 중 하나와 nesting/quoting/precedence/deduplication 위험 중 하나가 함께 있다.
- `variant_product`: 독립 variant 축이 2개 이상이고 조합별 검증이 필요하다.
- 포맷 변경, plan/review evidence 기록, 명령 출력 정리, 환경·도구 실패, 단일 국소 assertion, 동작 계약을 바꾸지 않는 기계적 rename/pass-through 전파는 signature 근거에서 제외한다.
- signature가 하나 이상이면 `triggered: true`, 없으면 `triggered: false`다. signature 개수는 audit 정보일 뿐 grade 점수나 위험 단계가 아니다.
- 이 판정은 현재 최초 범위의 구조만 사용한다. 과거 실패 횟수, review finding 개수, 이전 route는 signature를 추가하지 않는다.
- Step 3의 `needs_evidence``blocked`가 항상 우선한다. routed 가능한 `build`에는 다음 effective route를 적용한다.
- `triggered: false`: `base_lane`, `base_grade`를 유지한다.
- `triggered: true`: `lane: cloud`, `grade: max(base_grade, G07)`을 적용한다.
- build G08~G10은 signature 개수가 아니라 Step 5의 다섯 차원 점수로만 선택한다.
- closure 또는 build loop-risk 필드가 하나라도 `unknown`이면 `needs_evidence`다. 이를 `false`로 낮추지 않는다.
- closure `false`가 권한, secret, 사용자 결정, 환경·의존성 또는 신뢰할 증거 부재라면 `blocked`다.
- closure `false`를 cloud capability가 해소할 수 있을 때만 `capability-gap` 후보로 둔다.
7. **공식 Review capability floor**
- review의 폐쇄성과 다섯 차원 점수는 계속 독립 평가해 `base_lane`, `base_grade`로 보존한다.
- 공식 review의 최종 lane은 항상 `cloud`, 최종 grade는 `max(base_grade, G09)`이다.
- build와 review를 함께 평가하고 최종 build가 `G09` 또는 `G10`이면 review floor를 상한인 `G10`으로 맞춘다. 따라서 grade headroom이 있으면 review가 build보다 항상 높다.
- 이 단계는 review 점수를 build 점수로 복사하지 않는다. 독립 점수와 official-review floor를 함께 출력한다.
- `review`만 단독 평가해도 같은 cloud-G09 minimum을 적용한다.
### 3. G 등급
8. **파일명 확정**
- `plan`처럼 build와 review를 함께 평가할 때는 다음 pair finalizer를 정확히 한 번 실행한다.
각 target을 다음 다섯 차원에서 독립적으로 `0`~`2`로 채점한다.
```bash
agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh pair \
<build-base-lane> <true|false> \
<build-scope> <build-state> <build-blast> <build-evidence> <build-verification> \
<review-base-lane> \
<review-scope> <review-state> <review-blast> <review-evidence> <review-verification>
```
- `scope_coupling`: 국소 / 여러 연결 지점 / 다중 모듈·계약
- `state_concurrency`: 무상태 / 상태 전이 / 공유 상태·순서·동시성
- `blast_irreversibility`: 내부 가역 / 다중 호출자·지속 상태 / 외부 계약·권한·비가역
- `evidence_diagnosis`: 직접 증거 / 복수 증거 조합 / 충돌·불신 증거 진단
- `verification_complexity`: 단일 명령 / 복수 단계·통합 / 외부·상호작용·장시간
- 두 번째 인자는 build의 `loop_risk.triggered` boolean만 전달한다. matched signature 목록이나 개수는 전달하지 않는다.
- stdout의 `finalizer`, `finalizer_mode`, `build_base_*`, `build_floor`, `build_*`, `review_base_*`, `review_floor`, `review_*` 값을 그대로 출력 계약과 canonical filename에 사용한다.
- pair finalizer는 triggered build의 cloud-G07 minimum, 공식 review의 cloud-G09 minimum, build G09~G10일 때 review G10을 결정적으로 적용한다.
- `build` 또는 `review` 단독 평가도 같은 policy finalizer의 해당 mode를 사용한다.
`grade = max(1, 점수 합)`이다. lane과 반복 횟수는 점수에 더하지 않는다.
```bash
agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh build \
<base-lane> <true|false> \
<scope_coupling> <state_concurrency> <blast_irreversibility> \
<evidence_diagnosis> <verification_complexity>
### 4. Route basis
agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh review \
<base-lane> \
<scope_coupling> <state_concurrency> <blast_irreversibility> \
<evidence_diagnosis> <verification_complexity>
```
build basis를 하나만 선택한다.
- 저수준 `finalize-task-route.sh`는 policy finalizer 내부 계산 전용이다. 호출자는 canonical route를 얻기 위해 직접 실행하지 않는다.
- 모델이 점수 합산, signature-count 변환, grade floor, review 상한 분기, zero-padding, prefix 조합을 직접 계산하지 않는다.
- 두 target의 폐쇄성과 grade 점수는 각자의 실행 범위로 독립 평가하고, 최종 review route에만 Step 7의 official-review floor를 적용한다.
- 모든 요청 target이 `routed`일 때만 filename을 반환한다.
- 결과 확정 뒤 scope, 검증, 증거, 소유권, 결정 조건이 달라지면 결과를 무효화하고 Step 1부터 다시 실행한다.
- `local-fit`: 모든 closure가 `true`이고 G01~G08
- `grade-boundary`: 모든 closure가 `true`이고 G09~G10
- `capability-gap`: 하나 이상의 closure `false`를 cloud가 실제로 해소함
## 실행 결과 검증
`capability-gap`에는 관측된 gap, source/test/diff/log 증거, cloud가 해소하는 방법을 모두 기록한다. 이 세 근거 없는 G01~G08 build cloud route는 금지한다. G09~G10에 실제 gap도 있으면 basis는 `capability-gap`으로 두고 grade 점수도 함께 보존한다.
- [ ] 모든 요청 target에 여섯 폐쇄성 값과 현재 증거 근거가 있는가
- [ ] 재평가가 `isolated-reassessment`로 실행되었고 새 결과 전까지 이전 평가가 노출·비교되지 않았는가
- [ ] `unknown` 또는 `blocked` 상태에서 lane/G/filename을 만들지 않았는가
- [ ] 이전 lane/G, 실패 횟수, finding 개수를 승계하거나 자동 승격 근거로 사용하지 않았는가
- [ ] 기본 lane을 먼저 정하고 기본 G 등급을 별도 차원 점수로 산정했는가
- [ ] build의 다섯 loop-risk 범주가 현재 구조 증거로 평가되고 제외 항목이 signature에서 빠졌는가
- [ ] signature 개수를 G 등급으로 변환하지 않고 triggered build에 cloud-G07 floor만 적용했는가
- [ ] review의 독립 점수를 보존하면서 최종 route에 cloud-G09 minimum을 적용했고 build가 G09~G10이면 review가 G10인가
- [ ] build/review 동시 평가에는 policy finalizer의 `pair` mode를 정확히 한 번 실행했고 stdout의 base/floor/final route와 filename을 그대로 사용했는가
- [ ] build/review filename이 policy finalizer 결과와 정확히 일치하는가
- 검증 실패 시: 결과를 `unrouted`로 폐기하고 누락된 현재 증거부터 보강한 뒤 전체 평가를 다시 수행한다.
review basis는 항상 `official-review`다. review G는 실제 복잡도 점수를 유지하고 G floor를 적용하지 않는다. lane은 `cloud`, 실행 target은 Codex `gpt-5.6-sol` xhigh로 고정한다. local worker의 자가검증은 공식 review를 대체하지 않는다.
### 5. Loop-risk audit
build의 다음 signature를 현재 증거로 기록한다.
- `temporal_state`: 상태 3개 이상과 partial/retry/cancel/recovery/stale/duplicate 경로
- `concurrent_consistency`: actor 2개 이상과 atomic snapshot/lock order/exactly-once/race-free 제약
- `boundary_contract`: component 3개 이상 또는 consumer 2개 이상과 validation/immutability/compatibility 제약
- `structured_interpretation`: parser/lexer/protocol assembly/multi-source merge와 nesting/quoting/precedence/deduplication 위험
- `variant_product`: 독립 variant 축 2개 이상과 조합 검증
signature와 `triggered`는 audit 정보다. lane/G를 바꾸지 않는다.
WARN/FAIL 후에는 현재 scope를 다시 평가한다. local에서 닫히면 local을 유지하고, plan/split/fixture/test 문제면 local에서 재구성한다. 실제 capability gap만 해당 새 PLAN 세대를 cloud로 보낸다. 고정 시도 횟수나 finding 재발로 cloud를 확정하지 않는다.
### 6. Canonical filename
모든 gate를 통과한 뒤 `pair` mode를 정확히 한 번 실행한다. finalizer는 lane을 입력받지 않고 route basis와 grade에서 lane을 산출한다.
```bash
agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh pair \
<build-route-basis> <true|false> \
<build-scope> <build-state> <build-blast> <build-evidence> <build-verification> \
official-review \
<review-scope> <review-state> <review-blast> <review-evidence> <review-verification>
```
`loop_risk.triggered`는 두 번째 인자에 전달하지만 route에는 영향을 주지 않는다. `unknown` 상태에서는 finalizer를 호출하지 않는다. 호출자는 저수준 formatter를 직접 실행하거나 lane, grade, filename을 수작업으로 만들지 않는다.
## 검증
- [ ] 두 target의 closure, 근거와 점수가 독립적인가
- [ ] closure나 loop-risk의 `unknown`에서 finalizer를 호출하지 않았는가
- [ ] G01~G08 build cloud route가 `capability-gap`이고 세 근거가 있는가
- [ ] build의 `local-fit`은 G01~G08, `grade-boundary`는 G09~G10인가
- [ ] review가 `official-review`, cloud, Codex `gpt-5.6-sol` xhigh인가
- [ ] signature와 반복 횟수가 route에 영향을 주지 않았는가
- [ ] finalizer 출력과 active filename이 일치하는가
## 출력 형식
기존 호출자 호환성을 위해 최종 `lane`, `grade_scores`, `grade`, `filename`은 target 최상위에 유지한다. 두 target의 독립 평가를 `base_route`로 남기고, build에는 `loop_risk`, review에는 공식 상위 모델을 뜻하는 `capability_floor`를 추가한다. `grade_scores`는 기본 차원 점수이므로 floor 적용 시 최종 `grade`보다 합이 작을 수 있으며, 그 차이는 해당 floor field로 설명한다.
```yaml
status: routed | needs_evidence | blocked
evaluation_mode: first-pass | isolated-reassessment
@ -176,87 +129,34 @@ finalizer: finalize-task-policy.sh
finalizer_mode: pair
targets:
build:
closures:
scope_closed: true
context_closed: true
verification_closed: true
evidence_trusted: true
ownership_closed: true
decision_closed: true
closure_basis:
scope_closed: "<current evidence>"
context_closed: "<current evidence>"
verification_closed: "<current evidence>"
evidence_trusted: "<current evidence>"
ownership_closed: "<current evidence>"
decision_closed: "<current evidence>"
base_route:
lane: local
grade: G04
loop_risk:
matched_signatures: [boundary_contract, structured_interpretation, variant_product]
triggered: true
grade_floor: G07
basis:
boundary_contract: "<current structural evidence>"
structured_interpretation: "<current structural evidence>"
variant_product: "<current structural evidence>"
lane: cloud
grade_scores:
scope_coupling: 1
state_concurrency: 1
blast_irreversibility: 1
evidence_diagnosis: 0
verification_complexity: 1
grade: G07
filename: PLAN-cloud-G07.md
closures: {scope_closed: true, context_closed: true, verification_closed: true, evidence_trusted: true, ownership_closed: true, decision_closed: true}
closure_basis: {scope_closed: "<evidence>", context_closed: "<evidence>", verification_closed: "<evidence>", evidence_trusted: "<evidence>", ownership_closed: "<evidence>", decision_closed: "<evidence>"}
route_basis: local-fit
capability_gap: none
grade_scores: {scope_coupling: 2, state_concurrency: 1, blast_irreversibility: 1, evidence_diagnosis: 1, verification_complexity: 1}
loop_risk: {matched_signatures: [boundary_contract], triggered: true}
lane: local
grade: G06
filename: PLAN-local-G06.md
review:
closures:
scope_closed: true
context_closed: false
verification_closed: true
evidence_trusted: false
ownership_closed: true
decision_closed: true
closure_basis:
scope_closed: "<current evidence>"
context_closed: "<current evidence>"
verification_closed: "<current evidence>"
evidence_trusted: "<current evidence>"
ownership_closed: "<current evidence>"
decision_closed: "<current evidence>"
base_route:
lane: cloud
grade: G06
capability_floor:
source: official_review
lane: cloud
grade: G09
lane: cloud
grade_scores:
scope_coupling: 1
state_concurrency: 1
blast_irreversibility: 1
evidence_diagnosis: 2
verification_complexity: 1
grade: G09
filename: CODE_REVIEW-cloud-G09.md
closures: {scope_closed: true, context_closed: true, verification_closed: true, evidence_trusted: true, ownership_closed: true, decision_closed: true}
closure_basis: {scope_closed: "<evidence>", context_closed: "<evidence>", verification_closed: "<evidence>", evidence_trusted: "<evidence>", ownership_closed: "<evidence>", decision_closed: "<evidence>"}
route_basis: local-fit
capability_gap: none
grade_scores: {scope_coupling: 1, state_concurrency: 1, blast_irreversibility: 1, evidence_diagnosis: 1, verification_complexity: 1}
lane: local
grade: G05
filename: CODE_REVIEW-local-G05.md
missing_evidence: []
blocked_reason: null
```
`needs_evidence` 또는 `blocked`에서는 `finalizer: null`, `finalizer_mode: null`로 두고, 모든 target의 `base_route`, `lane`, `grade_scores`, `grade`, `filename`과 build의 `loop_risk`, review의 `capability_floor` `null`로 둔다.
`capability-gap`이면 `capability_gap``observed`, `evidence`, `cloud_resolution`을 기록한다. `needs_evidence`/`blocked`에서는 finalizer와 route 결과를 `null`로 둔다.
## 금지 사항
- 이전 lane/G를 기본값, 최소값, 비교 기준으로 사용하지 않는다.
- 기존 평가가 있는 재평가를 같은 평가 context에서 수행하지 않는다.
- local 실패를 이유로 cloud로 자동 승격하지 않는다.
- 높은 local G로 cloud에 필요한 컨텍스트나 판단을 보상하지 않는다.
- 파일 수, actor 수, 상태 수, dirty 상태, Required 개수 같은 단일 사실만으로 lane을 정하지 않는다. build 사전 판정은 정의된 복합 signature 전체 조건을 충족해야 한다.
- signature 개수로 G08~G10을 선택하거나 여러 signature를 grade 점수처럼 합산하지 않는다.
- 공식 review를 `local` 또는 G08 이하로 확정하지 않는다. build가 G09~G10이면 review를 G09로 두지 않는다.
- grade나 filename을 policy finalizer 출력과 다르게 수작업으로 만들지 않는다.
- `needs_evidence``cloud`로 치환하거나 미완성 상태에서 routed filename을 만들지 않는다.
- 격리 재평가를 위해 sub-agent/session을 만들거나 collaboration tool 또는 `wait`를 호출하지 않는다.
- PLAN, CODE_REVIEW, archive, complete.log 파일을 직접 생성·수정·이동하지 않는다.
- 이전 route나 반복 횟수로 자동 승격하지 않는다.
- signature로 cloud lane 또는 G floor를 강제하지 않는다.
- cloud가 해소할 수 없는 문제를 cloud route로 바꾸지 않는다.
- review에 universal cloud/G floor를 적용하지 않는다.
- PLAN, CODE_REVIEW, archive와 complete.log를 직접 수정하지 않는다.

View file

@ -1,4 +1,4 @@
interface:
display_name: "Finalize Task Routing"
short_description: "Finalize routes with capability floors"
default_prompt: "Use $finalize-task-routing after task analysis to score build/review independently, apply the build loop-risk floor and official review upper-tier floor, and return canonical filenames."
short_description: "Finalize local-first task routes"
default_prompt: "Use $finalize-task-routing after task analysis to evaluate build/review independently, use cloud only for evidenced capability gaps or G09-G10 complexity, and return canonical filenames."

View file

@ -4,9 +4,8 @@ set -euo pipefail
usage() {
printf '%s\n' \
'usage:' \
' finalize-task-policy.sh pair <build-base-lane> <true|false> <build-scope> <build-state> <build-blast> <build-evidence> <build-verification> <review-base-lane> <review-scope> <review-state> <review-blast> <review-evidence> <review-verification>' \
' finalize-task-policy.sh build <base-lane> <true|false> <scope> <state> <blast> <evidence> <verification>' \
' finalize-task-policy.sh review <base-lane> <scope> <state> <blast> <evidence> <verification>' >&2
' finalize-task-policy.sh pair <build-route-basis> <true|false> <build-scope> <build-state> <build-blast> <build-evidence> <build-verification> official-review <review-scope> <review-state> <review-blast> <review-evidence> <review-verification>' \
' build-route-basis: local-fit | capability-gap | grade-boundary' >&2
exit 2
}
@ -26,112 +25,91 @@ field() {
}
finalize_build() {
local base_lane=$1
local risk_triggered=$2
shift 2
local route_basis=$1
shift
local scores=("$@")
local base
local final
local lane
local route
case "$risk_triggered" in
true|false)
case "$route_basis" in
local-fit)
lane=local
;;
capability-gap|grade-boundary)
lane=cloud
;;
*)
usage
;;
esac
base=$("$formatter" build "$base_lane" "${scores[@]}")
BUILD_BASE_LANE=$(field "$base" lane)
BUILD_BASE_GRADE=$(field "$base" grade)
BUILD_RISK_TRIGGERED=$risk_triggered
route=$("$formatter" build "$lane" "${scores[@]}")
BUILD_ROUTE_BASIS=$route_basis
BUILD_LANE=$(field "$route" lane)
BUILD_GRADE=$(field "$route" grade)
BUILD_FILENAME=$(field "$route" filename)
if [[ "$risk_triggered" == true ]]; then
BUILD_FLOOR=G07
final=$("$formatter" build cloud "${scores[@]}" 7)
else
BUILD_FLOOR=none
final=$base
if [[ "$route_basis" == local-fit &&
( "$BUILD_GRADE" == G09 || "$BUILD_GRADE" == G10 ) ]]; then
printf 'local-fit is valid only for G01-G08\n' >&2
exit 2
fi
if [[ "$route_basis" == grade-boundary &&
"$BUILD_GRADE" != G09 && "$BUILD_GRADE" != G10 ]]; then
printf 'grade-boundary is valid only for G09-G10\n' >&2
exit 2
fi
BUILD_LANE=$(field "$final" lane)
BUILD_GRADE=$(field "$final" grade)
BUILD_FILENAME=$(field "$final" filename)
}
finalize_review() {
local base_lane=$1
local floor_number=$2
shift 2
local route_basis=$1
shift
local scores=("$@")
local base
local final
local route
base=$("$formatter" review "$base_lane" "${scores[@]}")
REVIEW_BASE_LANE=$(field "$base" lane)
REVIEW_BASE_GRADE=$(field "$base" grade)
REVIEW_FLOOR=$(printf 'G%02d' "$floor_number")
final=$("$formatter" review cloud "${scores[@]}" "$floor_number")
REVIEW_LANE=$(field "$final" lane)
REVIEW_GRADE=$(field "$final" grade)
REVIEW_FILENAME=$(field "$final" filename)
[[ "$route_basis" == official-review ]] || usage
route=$("$formatter" review cloud "${scores[@]}")
REVIEW_ROUTE_BASIS=$route_basis
REVIEW_LANE=$(field "$route" lane)
REVIEW_GRADE=$(field "$route" grade)
REVIEW_FILENAME=$(field "$route" filename)
REVIEW_ADAPTER=codex
REVIEW_MODEL=gpt-5.6-sol
REVIEW_REASONING_EFFORT=xhigh
}
emit_build() {
printf 'build_base_lane=%s\n' "$BUILD_BASE_LANE"
printf 'build_base_grade=%s\n' "$BUILD_BASE_GRADE"
printf 'build_route_basis=%s\n' "$BUILD_ROUTE_BASIS"
printf 'build_risk_triggered=%s\n' "$BUILD_RISK_TRIGGERED"
printf 'build_floor=%s\n' "$BUILD_FLOOR"
printf 'build_lane=%s\n' "$BUILD_LANE"
printf 'build_grade=%s\n' "$BUILD_GRADE"
printf 'build_filename=%s\n' "$BUILD_FILENAME"
}
emit_review() {
printf 'review_base_lane=%s\n' "$REVIEW_BASE_LANE"
printf 'review_base_grade=%s\n' "$REVIEW_BASE_GRADE"
printf 'review_floor=%s\n' "$REVIEW_FLOOR"
printf 'review_route_basis=%s\n' "$REVIEW_ROUTE_BASIS"
printf 'review_lane=%s\n' "$REVIEW_LANE"
printf 'review_grade=%s\n' "$REVIEW_GRADE"
printf 'review_filename=%s\n' "$REVIEW_FILENAME"
printf 'review_adapter=%s\n' "$REVIEW_ADAPTER"
printf 'review_model=%s\n' "$REVIEW_MODEL"
printf 'review_reasoning_effort=%s\n' "$REVIEW_REASONING_EFFORT"
}
mode=${1:-}
case "$mode" in
pair)
[[ $# -eq 14 ]] || usage
finalize_build "$2" "$3" "${@:4:5}"
if [[ "$BUILD_GRADE" == G09 || "$BUILD_GRADE" == G10 ]]; then
review_floor=10
else
review_floor=9
fi
finalize_review "$9" "$review_floor" "${@:10:5}"
;;
build)
[[ $# -eq 8 ]] || usage
finalize_build "$2" "$3" "${@:4:5}"
;;
review)
[[ $# -eq 7 ]] || usage
finalize_review "$2" 9 "${@:3:5}"
[[ "$mode" == pair && $# -eq 14 ]] || usage
case "$3" in
true|false)
BUILD_RISK_TRIGGERED=$3
;;
*)
usage
;;
esac
finalize_build "$2" "${@:4:5}"
finalize_review "$9" "${@:10:5}"
printf 'finalizer=finalize-task-policy.sh\n'
printf 'finalizer_mode=%s\n' "$mode"
case "$mode" in
pair)
emit_build
emit_review
;;
build)
emit_build
;;
review)
emit_review
;;
esac
emit_build
emit_review

View file

@ -3,18 +3,17 @@ set -euo pipefail
usage() {
printf '%s\n' \
'usage: finalize-task-route.sh <build|review> <local|cloud> <scope> <state> <blast> <evidence> <verification> [min-grade]' >&2
'usage: finalize-task-route.sh <build|review> <local|cloud> <scope> <state> <blast> <evidence> <verification>' >&2
exit 2
}
if [[ $# -ne 7 && $# -ne 8 ]]; then
if [[ $# -ne 7 ]]; then
usage
fi
target=$1
lane=$2
scores=("${@:3:5}")
min_grade=${8-1}
case "$target" in
build)
@ -36,14 +35,6 @@ case "$lane" in
;;
esac
case "$min_grade" in
1|2|3|4|5|6|7|8|9|10)
;;
*)
usage
;;
esac
sum=0
for score in "${scores[@]}"; do
case "$score" in
@ -60,10 +51,6 @@ if [[ $sum -lt 1 ]]; then
sum=1
fi
if [[ $sum -lt $min_grade ]]; then
sum=$min_grade
fi
printf 'target=%s\n' "$target"
printf 'lane=%s\n' "$lane"
printf 'grade=G%02d\n' "$sum"

View file

@ -25,7 +25,9 @@ GRADE_VECTORS = {
}
def run(script: Path, *args: object, check: bool = True) -> subprocess.CompletedProcess[str]:
def run(
script: Path, *args: object, check: bool = True
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[str(script), *(str(arg) for arg in args)],
check=check,
@ -39,275 +41,149 @@ def fields(stdout: str) -> dict[str, str]:
return dict(line.split("=", 1) for line in stdout.splitlines())
def expected_build_route(basis: str, grade: int) -> tuple[str, bool]:
if basis == "local-fit":
return "local", grade <= 8
if basis == "capability-gap":
return "cloud", True
return "cloud", grade >= 9
def pair_args(
build_basis: str,
build_grade: int,
review_grade: int,
risk: str = "false",
review_basis: str = "official-review",
) -> tuple[object, ...]:
return (
"pair",
build_basis,
risk,
*GRADE_VECTORS[build_grade],
review_basis,
*GRADE_VECTORS[review_grade],
)
class FinalizeTaskRoutingTests(unittest.TestCase):
def test_formatter_all_score_vectors_without_floor(self) -> None:
for target, lane, scores in itertools.product(
("build", "review"),
("local", "cloud"),
itertools.product(range(3), repeat=5),
):
with self.subTest(target=target, lane=lane, scores=scores):
result = fields(run(FORMATTER, target, lane, *scores).stdout)
expected_grade = max(1, sum(scores))
prefix = "PLAN" if target == "build" else "CODE_REVIEW"
self.assertEqual(result["grade"], f"G{expected_grade:02d}")
self.assertEqual(
result["filename"],
f"{prefix}-{lane}-G{expected_grade:02d}.md",
)
def test_formatter_exhaustive_grade_floor_matrix(self) -> None:
for target, lane, base_grade, floor in itertools.product(
("build", "review"),
("local", "cloud"),
range(1, 11),
range(1, 11),
):
with self.subTest(
target=target,
lane=lane,
base_grade=base_grade,
floor=floor,
):
result = fields(
run(
FORMATTER,
target,
lane,
*GRADE_VECTORS[base_grade],
floor,
).stdout
)
expected_grade = max(base_grade, floor)
prefix = "PLAN" if target == "build" else "CODE_REVIEW"
self.assertEqual(result["target"], target)
self.assertEqual(result["lane"], lane)
self.assertEqual(result["grade"], f"G{expected_grade:02d}")
self.assertEqual(
result["filename"],
f"{prefix}-{lane}-G{expected_grade:02d}.md",
)
def test_pair_exhaustive_route_matrix(self) -> None:
for (
build_lane,
risk_triggered,
build_base_grade,
review_base_lane,
review_base_grade,
) in itertools.product(
("local", "cloud"),
("false", "true"),
range(1, 11),
("local", "cloud"),
range(1, 11),
):
with self.subTest(
build_lane=build_lane,
risk_triggered=risk_triggered,
build_base_grade=build_base_grade,
review_base_lane=review_base_lane,
review_base_grade=review_base_grade,
):
result = fields(
run(
POLICY_FINALIZER,
"pair",
build_lane,
risk_triggered,
*GRADE_VECTORS[build_base_grade],
review_base_lane,
*GRADE_VECTORS[review_base_grade],
).stdout
)
if risk_triggered == "true":
expected_build_lane = "cloud"
expected_build_grade = max(build_base_grade, 7)
expected_build_floor = "G07"
else:
expected_build_lane = build_lane
expected_build_grade = build_base_grade
expected_build_floor = "none"
expected_review_floor = (
10 if expected_build_grade >= 9 else 9
)
expected_review_grade = max(
review_base_grade,
expected_review_floor,
)
self.assertEqual(result["build_base_lane"], build_lane)
self.assertEqual(
result["finalizer"],
"finalize-task-policy.sh",
)
self.assertEqual(result["finalizer_mode"], "pair")
self.assertEqual(
result["build_base_grade"],
f"G{build_base_grade:02d}",
)
self.assertEqual(
result["build_risk_triggered"],
risk_triggered,
)
self.assertEqual(result["build_floor"], expected_build_floor)
self.assertEqual(result["build_lane"], expected_build_lane)
self.assertEqual(
result["build_grade"],
f"G{expected_build_grade:02d}",
)
self.assertEqual(
result["build_filename"],
f"PLAN-{expected_build_lane}-G{expected_build_grade:02d}.md",
)
self.assertEqual(result["review_base_lane"], review_base_lane)
self.assertEqual(
result["review_base_grade"],
f"G{review_base_grade:02d}",
)
self.assertEqual(
result["review_floor"],
f"G{expected_review_floor:02d}",
)
self.assertEqual(result["review_lane"], "cloud")
self.assertEqual(
result["review_grade"],
f"G{expected_review_grade:02d}",
)
self.assertEqual(
result["review_filename"],
f"CODE_REVIEW-cloud-G{expected_review_grade:02d}.md",
)
def test_raw_sum_one_stays_g01(self) -> None:
result = fields(
run(FORMATTER, "build", "local", 1, 0, 0, 0, 0).stdout
def assert_route(
self,
result: dict[str, str],
target: str,
basis: str,
lane: str,
grade: int,
) -> None:
prefix = "PLAN" if target == "build" else "CODE_REVIEW"
self.assertEqual(result[f"{target}_route_basis"], basis)
self.assertEqual(result[f"{target}_lane"], lane)
self.assertEqual(result[f"{target}_grade"], f"G{grade:02d}")
self.assertEqual(
result[f"{target}_filename"], f"{prefix}-{lane}-G{grade:02d}.md"
)
self.assertEqual(result["grade"], "G01")
def test_single_target_policy_matrix(self) -> None:
for lane, risk_triggered, base_grade in itertools.product(
("local", "cloud"),
("false", "true"),
range(1, 11),
def test_formatter_grade_and_filename_matrix(self) -> None:
for target, lane, grade in itertools.product(
("build", "review"), ("local", "cloud"), range(1, 11)
):
with self.subTest(
target="build",
lane=lane,
risk_triggered=risk_triggered,
base_grade=base_grade,
):
result = fields(
run(
POLICY_FINALIZER,
"build",
lane,
risk_triggered,
*GRADE_VECTORS[base_grade],
).stdout
)
expected_lane = "cloud" if risk_triggered == "true" else lane
expected_grade = (
max(base_grade, 7)
if risk_triggered == "true"
else base_grade
)
expected_floor = (
"G07" if risk_triggered == "true" else "none"
)
with self.subTest(target=target, lane=lane, grade=grade):
result = fields(run(FORMATTER, target, lane, *GRADE_VECTORS[grade]).stdout)
prefix = "PLAN" if target == "build" else "CODE_REVIEW"
self.assertEqual(result["grade"], f"G{grade:02d}")
self.assertEqual(
result["finalizer"],
"finalize-task-policy.sh",
)
self.assertEqual(result["finalizer_mode"], "build")
self.assertEqual(result["build_base_lane"], lane)
self.assertEqual(
result["build_base_grade"],
f"G{base_grade:02d}",
)
self.assertEqual(
result["build_risk_triggered"],
risk_triggered,
)
self.assertEqual(result["build_floor"], expected_floor)
self.assertEqual(result["build_lane"], expected_lane)
self.assertEqual(
result["build_grade"],
f"G{expected_grade:02d}",
)
self.assertEqual(
result["build_filename"],
f"PLAN-{expected_lane}-G{expected_grade:02d}.md",
result["filename"], f"{prefix}-{lane}-G{grade:02d}.md"
)
for lane, base_grade in itertools.product(
("local", "cloud"),
range(1, 11),
):
with self.subTest(
target="review",
lane=lane,
base_grade=base_grade,
):
result = fields(
run(
POLICY_FINALIZER,
"review",
lane,
*GRADE_VECTORS[base_grade],
).stdout
)
expected_grade = max(base_grade, 9)
self.assertEqual(
result["finalizer"],
"finalize-task-policy.sh",
)
self.assertEqual(result["finalizer_mode"], "review")
self.assertEqual(result["review_base_lane"], lane)
self.assertEqual(
result["review_base_grade"],
f"G{base_grade:02d}",
)
self.assertEqual(result["review_floor"], "G09")
self.assertEqual(result["review_lane"], "cloud")
self.assertEqual(
result["review_grade"],
f"G{expected_grade:02d}",
)
self.assertEqual(
result["review_filename"],
f"CODE_REVIEW-cloud-G{expected_grade:02d}.md",
)
def test_invalid_inputs_are_rejected(self) -> None:
invalid_formatter_cases = (
("deploy", "local", 0, 0, 0, 0, 0),
("build", "hybrid", 0, 0, 0, 0, 0),
("build", "local", 3, 0, 0, 0, 0),
("build", "local", 0, 0, 0, 0, 0, 0),
("review", "cloud", 0, 0, 0, 0, 0, 11),
("review", "cloud", 0, 0, 0, 0, 0, "G09"),
)
for args in invalid_formatter_cases:
with self.subTest(formatter_args=args):
def test_formatter_invalid_inputs_are_rejected(self) -> None:
valid = list(GRADE_VECTORS[4])
invalid_cases: list[tuple[object, ...]] = [
("deploy", "local", *valid),
("build", "hybrid", *valid),
("build", "local", *valid[:-1]),
("build", "local", *valid, 1),
]
for index in range(5):
scores = valid.copy()
scores[index] = 3
invalid_cases.append(("build", "local", *scores))
for args in invalid_cases:
with self.subTest(args=args):
self.assertEqual(run(FORMATTER, *args, check=False).returncode, 2)
valid_scores = GRADE_VECTORS[4]
invalid_policy_cases = (
("pair", "local", "yes", *valid_scores, "local", *valid_scores),
("pair", "local", "unknown", *valid_scores, "local", *valid_scores),
("pair", "hybrid", "true", *valid_scores, "local", *valid_scores),
("pair", "local", "true", *valid_scores, "remote", *valid_scores),
("pair", "local", "true", 3, 0, 0, 0, 0, "local", *valid_scores),
("pair", "local", "true", *valid_scores, "local", *valid_scores, 2),
("build", "local", "unknown", *valid_scores),
("review", "local", "false", *valid_scores),
("review", "local", *valid_scores[:-1]),
("deploy", "local", *valid_scores),
def test_pair_policy_validates_each_build_basis_and_grade(self) -> None:
for basis, grade in itertools.product(
("local-fit", "capability-gap", "grade-boundary"), range(1, 11)
):
with self.subTest(basis=basis, grade=grade):
completed = run(
POLICY_FINALIZER,
*pair_args(basis, grade, 1),
check=False,
)
lane, valid = expected_build_route(basis, grade)
if not valid:
self.assertEqual(completed.returncode, 2)
continue
self.assertEqual(completed.returncode, 0, completed.stderr)
result = fields(completed.stdout)
self.assertEqual(result["finalizer_mode"], "pair")
self.assert_route(result, "build", basis, lane, grade)
def test_official_review_keeps_grade_and_fixes_execution_target(self) -> None:
for grade in range(1, 11):
with self.subTest(grade=grade):
result = fields(
run(POLICY_FINALIZER, *pair_args("local-fit", 1, grade)).stdout
)
self.assert_route(
result, "review", "official-review", "cloud", grade
)
self.assertEqual(result["review_adapter"], "codex")
self.assertEqual(result["review_model"], "gpt-5.6-sol")
self.assertEqual(result["review_reasoning_effort"], "xhigh")
def test_low_grade_cloud_requires_capability_gap_basis(self) -> None:
rejected = run(
POLICY_FINALIZER,
*pair_args("grade-boundary", 4, 4),
check=False,
)
for args in invalid_policy_cases:
with self.subTest(policy_args=args):
self.assertEqual(rejected.returncode, 2)
accepted = fields(
run(
POLICY_FINALIZER,
*pair_args("capability-gap", 4, 4),
).stdout
)
self.assert_route(accepted, "build", "capability-gap", "cloud", 4)
def test_loop_risk_is_audit_only(self) -> None:
outputs = []
for risk in ("false", "true"):
result = fields(
run(
POLICY_FINALIZER,
*pair_args("local-fit", 7, 6, risk),
).stdout
)
outputs.append(result)
self.assertEqual(result["build_risk_triggered"], risk)
for key in ("build_lane", "build_grade", "build_filename"):
self.assertEqual(outputs[0][key], outputs[1][key])
def test_policy_invalid_inputs_are_rejected(self) -> None:
valid = GRADE_VECTORS[4]
invalid_cases = (
("pair", "local-fit", "unknown", *valid, "official-review", *valid),
("pair", "local", "false", *valid, "official-review", *valid),
("pair", "local-fit", "false", *valid, "cloud", *valid),
("build", "local-fit", "false", *valid),
("pair", "local-fit", "false", *valid, "official-review", *valid[:-1]),
)
for args in invalid_cases:
with self.subTest(args=args):
self.assertEqual(
run(POLICY_FINALIZER, *args, check=False).returncode,
2,

View file

@ -0,0 +1,194 @@
#!/usr/bin/env python3
from __future__ import annotations
import itertools
import subprocess
import unittest
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parents[1]
FORMATTER = SKILL_DIR / "scripts" / "finalize-task-route.sh"
POLICY_FINALIZER = SKILL_DIR / "scripts" / "finalize-task-policy.sh"
GRADE_VECTORS = {
1: (0, 0, 0, 0, 0),
2: (2, 0, 0, 0, 0),
3: (2, 1, 0, 0, 0),
4: (2, 2, 0, 0, 0),
5: (2, 2, 1, 0, 0),
6: (2, 2, 2, 0, 0),
7: (2, 2, 2, 1, 0),
8: (2, 2, 2, 2, 0),
9: (2, 2, 2, 2, 1),
10: (2, 2, 2, 2, 2),
}
def run(
script: Path, *args: object, check: bool = True
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[str(script), *(str(arg) for arg in args)],
check=check,
capture_output=True,
text=True,
timeout=5,
)
def fields(stdout: str) -> dict[str, str]:
return dict(line.split("=", 1) for line in stdout.splitlines())
def expected_build_route(basis: str, grade: int) -> tuple[str, bool]:
if basis == "local-fit":
return "local", grade <= 8
if basis == "capability-gap":
return "cloud", True
return "cloud", grade >= 9
def pair_args(
build_basis: str,
build_grade: int,
review_grade: int,
risk: str = "false",
review_basis: str = "official-review",
) -> tuple[object, ...]:
return (
"pair",
build_basis,
risk,
*GRADE_VECTORS[build_grade],
review_basis,
*GRADE_VECTORS[review_grade],
)
class FinalizeTaskRoutingTests(unittest.TestCase):
def assert_route(
self,
result: dict[str, str],
target: str,
basis: str,
lane: str,
grade: int,
) -> None:
prefix = "PLAN" if target == "build" else "CODE_REVIEW"
self.assertEqual(result[f"{target}_route_basis"], basis)
self.assertEqual(result[f"{target}_lane"], lane)
self.assertEqual(result[f"{target}_grade"], f"G{grade:02d}")
self.assertEqual(
result[f"{target}_filename"], f"{prefix}-{lane}-G{grade:02d}.md"
)
def test_formatter_grade_and_filename_matrix(self) -> None:
for target, lane, grade in itertools.product(
("build", "review"), ("local", "cloud"), range(1, 11)
):
with self.subTest(target=target, lane=lane, grade=grade):
result = fields(run(FORMATTER, target, lane, *GRADE_VECTORS[grade]).stdout)
prefix = "PLAN" if target == "build" else "CODE_REVIEW"
self.assertEqual(result["grade"], f"G{grade:02d}")
self.assertEqual(
result["filename"], f"{prefix}-{lane}-G{grade:02d}.md"
)
def test_formatter_invalid_inputs_are_rejected(self) -> None:
valid = list(GRADE_VECTORS[4])
invalid_cases: list[tuple[object, ...]] = [
("deploy", "local", *valid),
("build", "hybrid", *valid),
("build", "local", *valid[:-1]),
("build", "local", *valid, 1),
]
for index in range(5):
scores = valid.copy()
scores[index] = 3
invalid_cases.append(("build", "local", *scores))
for args in invalid_cases:
with self.subTest(args=args):
self.assertEqual(run(FORMATTER, *args, check=False).returncode, 2)
def test_pair_policy_validates_each_build_basis_and_grade(self) -> None:
for basis, grade in itertools.product(
("local-fit", "capability-gap", "grade-boundary"), range(1, 11)
):
with self.subTest(basis=basis, grade=grade):
completed = run(
POLICY_FINALIZER,
*pair_args(basis, grade, 1),
check=False,
)
lane, valid = expected_build_route(basis, grade)
if not valid:
self.assertEqual(completed.returncode, 2)
continue
self.assertEqual(completed.returncode, 0, completed.stderr)
result = fields(completed.stdout)
self.assertEqual(result["finalizer_mode"], "pair")
self.assert_route(result, "build", basis, lane, grade)
def test_official_review_keeps_grade_and_fixes_execution_target(self) -> None:
for grade in range(1, 11):
with self.subTest(grade=grade):
result = fields(
run(POLICY_FINALIZER, *pair_args("local-fit", 1, grade)).stdout
)
self.assert_route(
result, "review", "official-review", "cloud", grade
)
self.assertEqual(result["review_adapter"], "codex")
self.assertEqual(result["review_model"], "gpt-5.6-sol")
self.assertEqual(result["review_reasoning_effort"], "xhigh")
def test_low_grade_cloud_requires_capability_gap_basis(self) -> None:
rejected = run(
POLICY_FINALIZER,
*pair_args("grade-boundary", 4, "local-fit", 4),
check=False,
)
self.assertEqual(rejected.returncode, 2)
accepted = fields(
run(
POLICY_FINALIZER,
*pair_args("capability-gap", 4, "local-fit", 4),
).stdout
)
self.assert_route(accepted, "build", "capability-gap", "cloud", 4)
def test_loop_risk_is_audit_only(self) -> None:
outputs = []
for risk in ("false", "true"):
result = fields(
run(
POLICY_FINALIZER,
*pair_args("local-fit", 7, "local-fit", 6, risk),
).stdout
)
outputs.append(result)
self.assertEqual(result["build_risk_triggered"], risk)
for key in ("build_lane", "build_grade", "build_filename"):
self.assertEqual(outputs[0][key], outputs[1][key])
def test_policy_invalid_inputs_are_rejected(self) -> None:
valid = GRADE_VECTORS[4]
invalid_cases = (
("pair", "local-fit", "unknown", *valid, "local-fit", *valid),
("pair", "local", "false", *valid, "local-fit", *valid),
("pair", "local-fit", "false", *valid, "cloud", *valid),
("build", "local-fit", "false", *valid),
("pair", "local-fit", "false", *valid, "local-fit", *valid[:-1]),
)
for args in invalid_cases:
with self.subTest(args=args):
self.assertEqual(
run(POLICY_FINALIZER, *args, check=False).returncode,
2,
)
if __name__ == "__main__":
unittest.main()

View file

@ -109,7 +109,7 @@ Task directory naming rules:
Final routing boundary:
- Do not estimate, inherit, or write lane/G while analyzing. Keep the task `unrouted` until all plan scope, split, verification, evidence, ownership, and decision facts are complete.
- During Step 2, collect the current build scope's raw `loop_risk_snapshot` facts defined by `finalize-task-routing`: ordered transitions and adverse paths, concurrent actors and consistency constraints, component/consumer boundaries and contract constraints, structured interpretation hazards, and independent variant axes. Record 0/false/empty only when absence is evidenced; otherwise record `unknown`. Do not classify signatures, risk level, lane, or grade in this skill.
- During Step 2, collect the current build scope's `loop_risk_snapshot` observations defined by `finalize-task-routing`. Record 0/false/empty only when absence is evidenced; otherwise use `unknown`. Any `unknown` requires more evidence in Step 3. Signatures remain audit evidence and never set lane or grade.
- Execute `agent-ops/skills/common/finalize-task-routing/SKILL.md` for both `build` and `review` after analysis. Its routed outputs are the only source for active PLAN/CODE_REVIEW filenames.
- Apply this boundary to first-pass plans, review WARN/FAIL follow-ups, USER_REVIEW replans, and each split subtask independently.
- Exception: an explicit `refine-local-plans` run may split an unstarted `PLAN-local-G??.md` into strict-subset local children once. Those children retain the original build/review lane, G, and canonical basenames without rerouting; normal plan creation and review follow-up never use this exception.
@ -201,7 +201,7 @@ Complete all items below before creating active plan/review files. Work through
- [ ] **Read all test files in full** — read every test file that exercises the changed behavior, including files implied by matched agent-test profiles when any are used and by the repository test layout.
- [ ] **Assess test coverage** — for each behavior change, explicitly record whether existing tests cover it.
- [ ] **Assess split boundaries first** — identify dependency boundaries, ownership boundaries, API-vs-call-site phases, risk profile splits, and independently verifiable behavior or contract slices before selecting plan files. Keep each production slice with its minimum required tests; use a test-only sibling only for additional integration or closure evidence. If any split gate applies or the decision is uncertain, write multiple subtask directories under one task group instead of one plan.
- [ ] **Capture build loop-risk facts** — after the split decision, fill every `loop_risk_snapshot` field defined by `finalize-task-routing` from the current subtask's source, tests, contracts, and verification plan. Use explicit 0/false/empty values only when absence is evidenced; otherwise use `unknown`. Do not classify signatures or choose a route here.
- [ ] **Capture build loop-risk observations** — after splitting, fill every `loop_risk_snapshot` field from source, tests, contracts, and verification. Use 0/false/empty only for evidenced absence; otherwise use `unknown`, which must be resolved before routing. Do not derive lane or grade here.
- [ ] **Resolve split predecessor completion** — if the selected or proposed subtask directory has `NN+PP[,QQ...]_...`, resolve each predecessor index under the same task group. Check only the active and archive candidate patterns defined in the task directory naming rules. Record found active/archive paths, missing predecessors, or ambiguous matches in `분석 결과 > 분할 판단` and, when order matters, `의존 관계 및 구현 순서`.
- [ ] **Grep all symbol references** — for any renamed or removed symbol, find every call site and import chain.
- [ ] **Check dependency manifests** — before adding any new package, verify its presence in go.mod / package manifest.
@ -217,7 +217,7 @@ This step is mandatory and must be the last semantic decision before routed file
- [ ] **Select evaluation mode** — use `first-pass` only when no prior routing exists. Review follow-ups, invalidated routes, and USER_REVIEW replans use `isolated-reassessment`: build a route-free neutral snapshot, seal it as the only evaluation input, and evaluate it in a scratch block inside the current invocation. This logical isolation does not require or permit a sub-agent, delegated session, collaboration tool, or wait loop.
- [ ] **Run the routing skill** — read and fully execute `agent-ops/skills/common/finalize-task-routing/SKILL.md` with both targets in the selected mode. Do not reproduce or replace its decision logic inside this skill. If isolated evaluation is unavailable, treat the routing result as `blocked`; do not silently evaluate in the current anchored context.
- [ ] **Resolve non-final states** — for `needs_evidence`, collect the named evidence and rerun from `unrouted`; for `blocked`, stop without mutating task files and report the exact release condition. In `prepare-follow-up`, leave the verdict-appended active pair in place so code-review recovery can resume without losing the loop state.
- [ ] **Accept only routed output** — require `finalizer: finalize-task-policy.sh`, `finalizer_mode: pair`, independent `build` and `review` closure records, base routes, grade scores, final lane/G, and canonical filenames, plus the build loop-risk result and official review cloud-G09 capability floor.
- [ ] **Accept only routed output** — require `finalizer: finalize-task-policy.sh`, `finalizer_mode: pair`, independent build/review closures, route basis, capability-gap records, grade scores, lane/G and canonical filenames. Reject a route basis that does not match its evidence and grade.
- [ ] **Freeze the result** — use those exact basenames in Steps 4-6. If any routing input changes before the files are written, discard both outputs and repeat this step from the beginning.
## Step 4 - Archive Existing Active Files
@ -285,7 +285,7 @@ Required sections:
- `심볼 참조`: list renamed/removed symbols and every call site found, or state "none" if no symbols were changed.
- `분할 판단`: state that the split decision policy was evaluated before choosing plan files. For a single plan, explain why each relevant split gate does not apply and why single-plan coordination is safer than splitting. For multi-plan output, list the shared task group plus each sibling subtask directory and dependency relationship. For dependent subtask plans, include each predecessor index and whether it is satisfied by an active or archived `complete.log`, missing, or ambiguous.
- `범위 결정 근거`: state which files or areas were explicitly excluded from this change and why. This is the boundary justification — the implementing agent must not silently expand scope beyond what is recorded here.
- `최종 라우팅`: record the `evaluation_mode`, `finalizer`, `finalizer_mode`, both targets' closure results, base routes, grade scores, final lane/G and canonical filenames, the build loop-risk result, and the official review cloud-G09 capability floor returned by `finalize-task-routing`. Do not include or compare a previous loop's lane/G or risk result.
- `최종 라우팅`: record the `evaluation_mode`, `finalizer`, `finalizer_mode`, both targets' closure results, base routes, grade scores, final lane/G and canonical filenames, the build loop-risk observations, capability-gap evidence, and `none` floor values returned by `finalize-task-routing`. Do not include or compare a previous loop's lane/G or risk result.
- `구현 체크리스트`: a top-level checklist the implementing agent must follow while coding. Include one item per implementation/verification unit; if the roadmap feature Task has `검증:`, keep that verification in the same checklist item instead of making a separate completion-criteria item. Include one item for whole-plan intermediate/final verification only when it is not already covered by the feature items. Make the last item exactly `- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.` Copy this checklist into the review stub's `구현 체크리스트` section with the same item text and order.
- One item per change: `### [TAG-1] Title`, `TAG-2`, etc.
- `수정 파일 요약`: table mapping files to item ids.
@ -380,7 +380,7 @@ Do not write or return a prepared pair when either routing target is not `routed
- Dependent split plans record predecessor completion using active sibling `complete.log` or matching archived `complete.log`; ambiguous archive matches are not guessed.
- Every plan item has problem, solution, checklist, test decision, and intermediate verification.
- The plan and review stub have matching `구현 체크리스트` item text/order; their final checkbox is the mandatory `CODE_REVIEW-*-G??.md` evidence item.
- `finalize-task-routing` ran after all analysis for both `build` and `review` in the required evaluation mode; its output recorded `finalizer: finalize-task-policy.sh` and `finalizer_mode: pair`; that mode ran exactly once; the build `loop_risk_snapshot` contained complete current raw facts; signature count was not converted to a grade; official review received cloud-G09 minimum and G10 when build was G09-G10; its output status is `routed`; and the active filenames plus `분석 결과 > 최종 라우팅` exactly match its two final results.
- `finalize-task-routing` ran after all analysis for both targets in the required mode; `pair` ran once; loop-risk has no `unknown`; repetition and signature counts did not change lane/G; each route basis matches its evidence and grade; G01~G08 cloud routes contain a resolvable capability gap; and the active filenames match the finalizer output.
- Review WARN/FAIL follow-ups entered through this plan skill and did not inherit or compare the archived lane/G.
- The plan's implementer instructions and review stub limit local implementation agents to implementation/test/evidence work and keep user-review classification plus control-plane stop files out of their input and ownership.
- The review stub has a clearly marked `코드리뷰 전용 체크리스트` owned only by the review agent.

View file

@ -1,4 +1,4 @@
interface:
display_name: "Plan"
short_description: "Write implementation plans"
default_prompt: "Use $plan to analyze this repository change, collect current build loop-risk facts, run $finalize-task-routing with the official review upper-tier floor, and create the routed PLAN/CODE_REVIEW pair."
default_prompt: "Use $plan to analyze this repository change, collect current routing evidence, run $finalize-task-routing with local-first capability-gap policy, and create the routed PLAN/CODE_REVIEW pair."

View file

@ -35,20 +35,19 @@ description: agent-task의 작업들 실행해 요청 시 활성 PLAN/CODE_REVIE
| PLAN route | Worker |
|---|---|
| `local-G01`~`local-G06` | Pi `iop/ornith-fast`, thinking high |
| `local-G07`~`local-G08` | Pi `iop/laguna-s:2.1`, thinking high |
| `local-G01`~`local-G06` | Pi `iop/ornith:35b`, thinking high |
| `local-G07`~`local-G08` | agy `Gemini 3.6 Flash (Medium)` |
| `local-G09`~`local-G10` | Claude `claude-opus-4-8`, effort xhigh |
| `cloud-G01`~`cloud-G02` | agy `Gemini 3.5 Flash (Low)` |
| `cloud-G03`~`cloud-G04` | agy `Gemini 3.5 Flash (Medium)` |
| `cloud-G05`~`cloud-G06` | agy `Gemini 3.5 Flash (High)` |
| `cloud-G01`~`cloud-G02` | agy `Gemini 3.6 Flash (Low)` |
| `cloud-G03`~`cloud-G04` | agy `Gemini 3.6 Flash (Medium)` |
| `cloud-G05`~`cloud-G06` | agy `Gemini 3.6 Flash (High)` |
| `cloud-G07`~`cloud-G08` | Claude `claude-opus-4-8`, effort xhigh |
| `cloud-G09`~`cloud-G10` | Codex `gpt-5.6-sol`, reasoning xhigh |
| 모든 `CODE_REVIEW-*` | Codex `gpt-5.6-sol`, reasoning xhigh |
동시 실행 제한:
- Pi `ornith-fast`: 3
- Pi `laguna-s:2.1`: 2
- Pi `ornith:35b`: 3
- agy: 1
- 공식 Codex review: 별도 숫자 제한 없음
- worker/selfcheck와 공식 review는 task가 다르고 dependency가 충족되면 write-set과 무관하게 병렬 실행한다. 같은 task의 중복 실행만 막는다.
@ -74,7 +73,7 @@ Pi 자가 재검토의 exit code `0`만으로 성공 처리하지 않는다. `CO
Pi 복구 attempt는 locator 설명을 전달하지 않고 `Read {PLAN_PATH}. Continue.`만 사용한다. 다른 CLI 승격 attempt에는 `Continue from {LOCATOR_PATH}. Check the saved context and current workspace. Final in Korean.`을 사용한다. 공식 review 승격·복구 프롬프트에는 collaboration 금지와 next-state materialization 문장을 유지한다. 모델에게 별도 handoff 요약 작성을 요구하지 않는다.
Pi `laguna-s`에서 `context-limit`/max-token 또는 이전 dispatcher가 남긴 legacy terminal `session-stall` locator를 복구하면 fresh session-id를 만들지 않는다. 직전 locator의 native session 파일을 `pi --session`과 기존 `--session-dir`로 그대로 재개하고 `Think in English. Final in Korean. Continue this session and complete the current task.`를 전달한다. dispatcher 재기동 뒤에도 failed locator를 찾아 같은 session을 재개한다. 이 same-session 재시작도 동일 stage의 연속 실패 10회 한도에 포함한다.
새 route에서 제외된 legacy Pi `laguna-s` `context-limit`/max-token 또는 이전 dispatcher가 남긴 terminal `session-stall` locator를 복구하면 fresh session-id를 만들지 않는다. 직전 locator의 native session 파일을 `pi --session`과 기존 `--session-dir`로 그대로 재개하고 `Think in English. Final in Korean. Continue this session and complete the current task.`를 전달한다. dispatcher 재기동 뒤에도 failed locator를 찾아 같은 session을 재개한다. 이 same-session 재시작도 동일 stage의 연속 실패 10회 한도에 포함한다.
## 작업 로그 계약
@ -105,11 +104,12 @@ Pi `laguna-s`에서 `context-limit`/max-token 또는 이전 dispatcher가 남긴
- 새 user turn이 와도 기존 요청을 취소하라는 명시가 없으면 caller가 같은 전체 작업을 계속 추적한다.
- 각 `작업시작`, `자가검증시작`, `리뷰시작`, `리뷰재시도`, `Pi복구재시도`, `세션응답복구재시도`, `세션연결재시도`, `리뷰결과`, `작업대기`, `작업차단`, `디스패치추적대기`, `작업완료` banner를 사용자 commentary에 전달한다. 새 banner가 없어도 실행 중이면 60초 안에 현재 stage와 마지막 locator·task-group `WORK_LOG.md` 경로를 한 번 표시한다.
- 모든 CLI의 health/progress는 실제 stdout/stderr `stream.log`를 우선으로, native session event가 있는 CLI는 그 event도 함께 본다. heartbeat mtime은 절대 진행 근거가 아니다. locator에는 dispatcher PID·agent PID와 각 process start token, attempt별 process environment marker를 기록하며, 다른 dispatcher는 PID/start token 또는 marker로 같은 process가 살아 있으면 조용한 stream만으로 중복 attempt를 시작하지 않는다. agent PID가 없는 locator도 stream/native 진행 증거가 하나라도 있으면 시간 경과만으로 stale 또는 중복 복구로 판정하지 않으며, 모든 모델에서 실제 terminal 오류나 확인된 process 종료만 복구 근거로 사용한다. Pi는 `--mode json`으로 실행해 `thinking_delta`/`text_delta`/tool stream을 stdout으로 남긴다. Pi의 **정확한** toolCall→모든 toolResult 구간은 직전 assistant의 모든 `toolCall.id`와 이후 `toolResult.toolCallId`가 일치할 때만 끝나며, 시간 제한으로 process를 종료하지 않는다. 이 구간에서 agent PID가 locator에 없으면 로그 시간이 아무리 오래되어도 stale로 판정하거나 중복 복구하지 않고, 기록된 process evidence가 종료된 경우에만 종료 근거로 삼는다. `starting`·`unknown`·모델 추론·toolResult 뒤 상태는 tool 실행으로 추정하지 않는다. 그 밖의 Pi liveness는 `stream.log` 갱신만 기준으로 하며 toolResult 자체는 모델 응답 silence clock을 초기화하지 않는다. tool 실행 구간 밖에서 stream이 3분 멈추면 dispatcher는 마지막 stream 일부를 `pi_silence_inspection`에, 다른 CLI는 `stream_silence_inspection`에 기록하고 `모델응답점검`을 출력하되 model process를 종료하지 않는다. 실제 terminal 오류·process 종료만 복구 근거로 사용한다.
- 로컬 모델의 `repetition-loop` 관찰은 `thinking_delta`/`text_delta`의 유사도나 의미 중복만으로 판정하지 않는다. 새 tool event·파일/state 변화 없이 같은 정규화 chunk가 3회 연속 반복될 때만 `repetition_loop_suspected`로 기록한다. 이 신호 하나로 process 종료, task blocker, 복구/retry 또는 모델 승격을 실행하지 않고 다음 실질 진행이나 실제 terminal 오류를 관찰한다.
- `provider-connection`, `provider-stream-disconnect`, `session-stall`, `generic-error`, `process-terminated`, context/quota/model 오류와 review control violation은 원인을 합치지 않되 동일 task stage의 연속 자동 복구 실패 예산 10회를 공유한다. 10번째 실패에서 해당 task를 차단하고 자동 cooldown 재개하지 않는다. 성공하면 그 stage의 누적 실패를 초기화한다.
- Pi 자가검증 결과가 10회 연속 구현 필드 미완성이거나 공식 review가 10회 연속 무변경이면 해당 task를 각각 명시적 terminal blocker로 기록한다.
- 하나의 task가 복구·차단되는 동안에도 scheduler는 그 task를 predecessor로 요구하지 않는 모든 ready/running task를 계속 실행한다. 내부 복구나 차단은 전체 후보군 스캔을 임의로 유발하지 않는다.
- review shared-state preflight 실패는 준비된 review task들만 차단하고 같은 pass의 worker/selfcheck는 모두 계속 시작한다. `complete.log` 직후 전체 스캔은 이미 실행 중인 task 디렉터리를 다시 읽지 않고 기존 snapshot을 보존하여 병렬 archive 이동과의 경합으로 다른 process를 중단시키지 않는다.
- `laguna-s``context-limit`/legacy `session-stall` Prompt 계약의 same-session 재개 규칙을 우선하고 `Pi세션연속재시작`으로 표시한다. 그 외 legacy Pi `session-stall` 복구만 fresh session과 `세션응답복구재시도`를 사용한다.
- 새 route에서 제외된 legacy `laguna-s` locator의 `context-limit`/`session-stall`만 Prompt 계약의 same-session 재개 규칙을 우선하고 `Pi세션연속재시작`으로 표시한다. 그 외 legacy Pi `session-stall` 복구만 fresh session과 `세션응답복구재시도`를 사용한다.
- user-review 정지는 파일명만으로 판정하지 않는다. active task의 `USER_REVIEW.md``상태: USER_REVIEW`, `유형: milestone-lock`, 실제 `agent-roadmap/**/milestones/*.md` 연결 대상, `없음`·`미정`이 아닌 차단 판단 근거·미해결 결정 항목·재개 조건을 모두 가져 현재 task의 다음 안전한 구현을 막을 때만 `user-review` terminal blocker로 인정한다. 형식이 불완전하거나 active PLAN/CODE_REVIEW와 충돌하면 user-review가 아니라 task 상태 계약 오류로 차단한다.
- review 판정은 `## 코드리뷰 결과` 섹션 안의 유일한 `종합 판정: PASS|WARN|FAIL` 필드만 인정한다. 구현 증거, 명령 출력, 예시 문구에 같은 문자열이 있어도 runtime verdict로 해석하지 않는다.
- `.git/agent-task-dispatcher/runs/`의 locator/raw log는 복구용 내부 상태라 일반 project tree에 보이지 않을 수 있다. dispatcher가 attempt 시작 즉시 출력하는 `locator=`와 task-group `WORK_LOG.md` 경로를 상태 업데이트에 포함한다.
@ -203,7 +203,7 @@ Pi `laguna-s`에서 `context-limit`/max-token 또는 이전 dispatcher가 남긴
------------------------------------------
작업시작: 03+01_event_contract_unit_tests
------------------------------------------
model=pi/iop/ornith-fast
model=pi/iop/ornith:35b
plan=/absolute/path/PLAN-local-G05.md
work_log=/absolute/path/WORK_LOG.md
[03+01_event_contract_unit_tests][worker][a00] ...

View file

@ -980,16 +980,20 @@ def route_agent(task: Task) -> AgentSpec:
grade = task.grade
if task.lane == "local":
if grade <= 6:
return AgentSpec("pi", "ornith-fast", "pi/iop/ornith-fast", local_pi=True)
return AgentSpec("pi", "ornith:35b", "pi/iop/ornith:35b", local_pi=True)
if grade <= 8:
return AgentSpec("pi", "laguna-s:2.1", "pi/iop/laguna-s:2.1", local_pi=True)
return AgentSpec(
"agy",
"Gemini 3.6 Flash (Medium)",
"agy/Gemini 3.6 Flash (Medium)",
)
return AgentSpec("claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh")
if grade <= 2:
return AgentSpec("agy", "Gemini 3.5 Flash (Low)", "agy/Gemini 3.5 Flash (Low)")
return AgentSpec("agy", "Gemini 3.6 Flash (Low)", "agy/Gemini 3.6 Flash (Low)")
if grade <= 4:
return AgentSpec("agy", "Gemini 3.5 Flash (Medium)", "agy/Gemini 3.5 Flash (Medium)")
return AgentSpec("agy", "Gemini 3.6 Flash (Medium)", "agy/Gemini 3.6 Flash (Medium)")
if grade <= 6:
return AgentSpec("agy", "Gemini 3.5 Flash (High)", "agy/Gemini 3.5 Flash (High)")
return AgentSpec("agy", "Gemini 3.6 Flash (High)", "agy/Gemini 3.6 Flash (High)")
if grade <= 8:
return AgentSpec("claude", "claude-opus-4-8", "claude/claude-opus-4-8 xhigh")
return AgentSpec("codex", "gpt-5.6-sol", "codex/gpt-5.6-sol xhigh")
@ -3753,8 +3757,7 @@ async def dispatch_with_store(
if args.retry_blocked and not args.dry_run:
store.clear_blocked(args.task_group)
semaphores = {
"pi:ornith-fast": asyncio.Semaphore(3),
"pi:laguna-s:2.1": asyncio.Semaphore(2),
"pi:ornith:35b": asyncio.Semaphore(3),
"agy": asyncio.Semaphore(1),
"claude": asyncio.Semaphore(64),
"codex": asyncio.Semaphore(64),

View file

@ -4,13 +4,13 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, time
from datetime import datetime
from zoneinfo import ZoneInfo
KST = ZoneInfo("Asia/Seoul")
DAY_START = time(7, 0)
NIGHT_START = time(23, 0)
VALID_STAGES = {"worker", "review"}
VALID_LANES = {"local", "cloud"}
@ -32,12 +32,16 @@ class PolicyDecision:
candidates: tuple[RouteTarget, ...]
PI_ORNITH = RouteTarget("pi", "iop/ornith-fast", "local_model", True)
PI_LAGUNA = RouteTarget("pi", "iop/laguna-s:2.1", "local_model", True)
AGY_GEMINI_MEDIUM = RouteTarget(
"agy", "Gemini 3.6 Flash Medium", "cloud_model", False
PI_ORNITH = RouteTarget("pi", "iop/ornith:35b", "local_model", True)
AGY_GEMINI_LOW = RouteTarget(
"agy", "Gemini 3.6 Flash (Low)", "cloud_model", False
)
AGY_GEMINI_MEDIUM = RouteTarget(
"agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False
)
AGY_GEMINI_HIGH = RouteTarget(
"agy", "Gemini 3.6 Flash (High)", "cloud_model", False
)
CLAUDE_SONNET = RouteTarget("claude", "sonnet", "cloud_model", False)
CLAUDE_OPUS = RouteTarget("claude", "claude-opus-4-8", "cloud_model", False)
CODEX_SOL_XHIGH = RouteTarget("codex", "gpt-5.6-sol", "cloud_model", False)
@ -53,14 +57,6 @@ def _validate(stage: str, lane: str, grade: int, evaluated_at: datetime) -> None
raise ValueError("evaluated_at must be timezone-aware")
def _kst_window(evaluated_at: datetime) -> tuple[str, bool]:
current = evaluated_at.astimezone(KST).time().replace(tzinfo=None)
is_day = DAY_START <= current < NIGHT_START
if is_day:
return "kst-day-[07:00,23:00)", True
return "kst-night-[23:00,07:00)", False
def select_policy(
*, stage: str, lane: str, grade: int, evaluated_at: datetime
) -> PolicyDecision:
@ -87,23 +83,12 @@ def select_policy(
candidates=(PI_ORNITH,),
)
if grade <= 8:
window, is_day = _kst_window(evaluated_at)
return PolicyDecision(
rule_id=(
"worker-local-g07-g08-day"
if is_day
else "worker-local-g07-g08-night"
),
rule_id="worker-local-g07-g08",
policy_priority=20,
reason_codes=(
"kst_day_window" if is_day else "kst_night_window",
),
time_window=window,
candidates=(
(AGY_GEMINI_MEDIUM, PI_LAGUNA)
if is_day
else (PI_LAGUNA, AGY_GEMINI_MEDIUM)
),
reason_codes=("explicit_gemini_route",),
time_window="not_applicable",
candidates=(AGY_GEMINI_MEDIUM,),
)
return PolicyDecision(
rule_id="worker-local-g09-g10",
@ -113,10 +98,18 @@ def select_policy(
candidates=(CLAUDE_OPUS,),
)
if grade <= 6:
target = CLAUDE_SONNET
rule_id = "worker-cloud-g01-g06"
reason_code = "cloud_sonnet_grade"
if grade <= 2:
target = AGY_GEMINI_LOW
rule_id = "worker-cloud-g01-g02"
reason_code = "cloud_gemini_low_grade"
elif grade <= 4:
target = AGY_GEMINI_MEDIUM
rule_id = "worker-cloud-g03-g04"
reason_code = "cloud_gemini_medium_grade"
elif grade <= 6:
target = AGY_GEMINI_HIGH
rule_id = "worker-cloud-g05-g06"
reason_code = "cloud_gemini_high_grade"
elif grade <= 8:
target = CLAUDE_OPUS
rule_id = "worker-cloud-g07-g08"

View file

@ -4,8 +4,8 @@
The selector consumes a static routing task file (``PLAN-*`` or
``CODE_REVIEW-*``), its ``task/plan/tag`` generation header and optional prior
decision / quota snapshot, and returns a stable JSON contract that the
dispatcher can persist. This module closes SDD S01~S03 (schema/invalid-input,
KST time route, worker/review grade matrix) at the selector surface. Failover
dispatcher can persist. This module exposes the schema/invalid-input and
worker/review grade matrix at the selector surface. Failover
target switching, quota probe execution and dispatcher persisted state are owned
by later tasks and are intentionally not implemented here.
"""
@ -34,6 +34,8 @@ _VALID_QUOTA_MODES = {"unbounded", "bounded"}
_VALID_QUOTA_STATUSES = {"not_applicable", "available", "exhausted", "unknown"}
_VALID_ELIGIBILITY = {"eligible", "ineligible"}
_VALID_REJECTION_REASONS = {"quota_exhausted"}
# Older persisted decisions may contain a KST window. New initial decisions use
# ``not_applicable``; legacy values remain valid only for resume compatibility.
_VALID_TIME_WINDOWS = {
"kst-day-[07:00,23:00)",
"kst-night-[23:00,07:00)",
@ -432,9 +434,12 @@ def _initial(
quota_snapshot: dict | None,
quota_probe_command: str,
) -> dict:
decision = policy.select_policy(
stage=stage, lane=lane, grade=grade, evaluated_at=evaluated_at
)
try:
decision = policy.select_policy(
stage=stage, lane=lane, grade=grade, evaluated_at=evaluated_at
)
except ValueError as exc:
raise SelectorInputError("invalid_route", str(exc)) from exc
candidates = []
selected = None
for rank, target in enumerate(decision.candidates, start=1):

View file

@ -262,10 +262,10 @@ class TaskStageTest(unittest.TestCase):
with tempfile.TemporaryDirectory() as temporary:
task = self.make_task(Path(temporary))
expected = {
5: ("pi", "ornith-fast", True),
6: ("pi", "ornith-fast", True),
7: ("pi", "laguna-s:2.1", True),
8: ("pi", "laguna-s:2.1", True),
5: ("pi", "ornith:35b", True),
6: ("pi", "ornith:35b", True),
7: ("agy", "Gemini 3.6 Flash (Medium)", False),
8: ("agy", "Gemini 3.6 Flash (Medium)", False),
9: ("claude", "claude-opus-4-8", False),
10: ("claude", "claude-opus-4-8", False),
}
@ -612,7 +612,7 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase):
return real_write_json(path, value)
spec = dispatch.AgentSpec(
"pi", "ornith-fast", "pi", local_pi=True
"pi", "ornith:35b", "pi", local_pi=True
)
try:
with (
@ -818,7 +818,7 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase):
)
return [sys.executable, "-c", child, str(native)]
spec = dispatch.AgentSpec("pi", "ornith-fast", "pi", local_pi=True)
spec = dispatch.AgentSpec("pi", "ornith:35b", "pi", local_pi=True)
try:
with (
mock.patch.object(dispatch, "build_command", side_effect=command_for),
@ -1188,7 +1188,7 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase):
provider_line,
]
spec = dispatch.AgentSpec(
"pi", "ornith-fast", "pi", local_pi=True
"pi", "ornith:35b", "pi", local_pi=True
)
try:
with mock.patch.object(
@ -1369,8 +1369,8 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase):
)
spec = dispatch.AgentSpec(
"agy",
"Gemini 3.5 Flash (High)",
"agy/Gemini 3.5 Flash (High)",
"Gemini 3.6 Flash (High)",
"agy/Gemini 3.6 Flash (High)",
)
def build_agy_command(
@ -1439,7 +1439,7 @@ class WorkLogInvokeIntegrationTest(unittest.IsolatedAsyncioTestCase):
"connect: connection refused"
)
spec = dispatch.AgentSpec(
"pi", "ornith-fast", "pi", local_pi=True
"pi", "ornith:35b", "pi", local_pi=True
)
try:
with mock.patch.object(
@ -2072,7 +2072,7 @@ class ReviewControlTest(unittest.TestCase):
self.assertIn("dispatcher_source_sha256", skill)
self.assertIn("`dispatcher_source_matches_loaded=false`", skill)
self.assertIn(
"`laguna-s`의 `context-limit`/legacy `session-stall`은 Prompt 계약의 same-session 재개 규칙을 우선",
"새 route에서 제외된 legacy `laguna-s` locator의 `context-limit`/`session-stall`만 Prompt 계약의 same-session 재개 규칙을 우선",
skill,
)
self.assertIn(
@ -2230,8 +2230,8 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
task = self.make_task(root)
agy = dispatch.AgentSpec(
"agy",
"Gemini 3.5 Flash (High)",
"agy/Gemini 3.5 Flash (High)",
"Gemini 3.6 Flash (High)",
"agy/Gemini 3.6 Flash (High)",
)
claude = dispatch.AgentSpec(
"claude",
@ -2343,8 +2343,8 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
self.assertIsNotNone(recovery)
initial_route = dispatch.AgentSpec(
"agy",
"Gemini 3.5 Flash (High)",
"agy/Gemini 3.5 Flash (High)",
"Gemini 3.6 Flash (High)",
"agy/Gemini 3.6 Flash (High)",
)
terra = dispatch.AgentSpec(
"codex",
@ -2399,7 +2399,7 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
store.runs,
task,
cli="agy",
model="Gemini 3.5 Flash (High)",
model="Gemini 3.6 Flash (High)",
reasoning_effort=None,
)[-1]
state = store.task_state(task)
@ -2421,8 +2421,8 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(recovery.evidence_source, "agy:cli-log")
initial_route = dispatch.AgentSpec(
"agy",
"Gemini 3.5 Flash (High)",
"agy/Gemini 3.5 Flash (High)",
"Gemini 3.6 Flash (High)",
"agy/Gemini 3.6 Flash (High)",
)
claude = dispatch.AgentSpec(
"claude",
@ -2476,8 +2476,8 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
store = dispatch.StateStore(root)
initial_route = dispatch.AgentSpec(
"agy",
"Gemini 3.5 Flash (High)",
"agy/Gemini 3.5 Flash (High)",
"Gemini 3.6 Flash (High)",
"agy/Gemini 3.6 Flash (High)",
)
attempt = store.runs / "completed-attempt"
attempt.mkdir()
@ -2655,7 +2655,7 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
task = self.make_task(root)
spec = dispatch.AgentSpec("pi", "ornith-fast", "pi", local_pi=True)
spec = dispatch.AgentSpec("pi", "ornith:35b", "pi", local_pi=True)
locators = [root / f"locator-{index}.json" for index in range(3)]
results = [
(1, "provider-connection", locators[0]),
@ -2682,7 +2682,7 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
task = self.make_task(root)
spec = dispatch.AgentSpec("pi", "ornith-fast", "pi", local_pi=True)
spec = dispatch.AgentSpec("pi", "ornith:35b", "pi", local_pi=True)
locators = [
root / f"locator-{index}.json"
for index in range(dispatch.RECOVERY_FAILURE_LIMIT)
@ -2734,7 +2734,7 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
store.update_task(
task, recovery_failures={"worker": dispatch.RECOVERY_FAILURE_LIMIT - 1}
)
spec = dispatch.AgentSpec("pi", "ornith-fast", "pi", local_pi=True)
spec = dispatch.AgentSpec("pi", "ornith:35b", "pi", local_pi=True)
try:
with mock.patch.object(
dispatch,
@ -2766,7 +2766,7 @@ class ReviewRetryTest(unittest.IsolatedAsyncioTestCase):
store.update_task(
task, recovery_failures={"worker": dispatch.RECOVERY_FAILURE_LIMIT}
)
spec = dispatch.AgentSpec("pi", "ornith-fast", "pi", local_pi=True)
spec = dispatch.AgentSpec("pi", "ornith:35b", "pi", local_pi=True)
try:
with mock.patch.object(
dispatch, "invoke", new=mock.AsyncMock()
@ -2828,7 +2828,7 @@ class RepetitionLimitTest(unittest.IsolatedAsyncioTestCase):
root,
store,
task,
{"pi:ornith-fast": asyncio.Semaphore(1)},
{"pi:ornith:35b": asyncio.Semaphore(1)},
)
self.assertEqual(run_escalating.await_count, 0)
self.assertIn(
@ -2887,7 +2887,7 @@ class RepetitionLimitTest(unittest.IsolatedAsyncioTestCase):
root,
store,
task,
{"pi:ornith-fast": asyncio.Semaphore(1)},
{"pi:ornith:35b": asyncio.Semaphore(1)},
)
self.assertEqual(run_escalating.await_count, 1)
self.assertIn(
@ -4625,7 +4625,7 @@ class OrchestrationPersistenceTest(unittest.TestCase):
runs,
task,
cli="agy",
model="Gemini 3.5 Flash (High)",
model="Gemini 3.6 Flash (High)",
reasoning_effort=None,
)[-1]
blocked_state = {

View file

@ -22,12 +22,12 @@ def at_utc(hour: int, minute: int = 0, second: int = 0) -> datetime:
class ExecutionTargetPolicyTests(unittest.TestCase):
def test_kst_boundary_routes(self):
def test_local_g07_route_is_time_independent(self):
cases = [
(at_utc(21, 59, 59), "pi", "iop/laguna-s:2.1"),
(at_utc(22, 0, 0), "agy", "Gemini 3.6 Flash Medium"),
(at_utc(13, 59, 59), "agy", "Gemini 3.6 Flash Medium"),
(at_utc(14, 0, 0), "pi", "iop/laguna-s:2.1"),
(at_utc(21, 59, 59), "agy", "Gemini 3.6 Flash (Medium)"),
(at_utc(22, 0, 0), "agy", "Gemini 3.6 Flash (Medium)"),
(at_utc(13, 59, 59), "agy", "Gemini 3.6 Flash (Medium)"),
(at_utc(14, 0, 0), "agy", "Gemini 3.6 Flash (Medium)"),
]
for evaluated_at, adapter, target in cases:
with self.subTest(evaluated_at=evaluated_at):
@ -45,18 +45,26 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
expected = {
"local": {
**{
grade: ("pi", "iop/ornith-fast", True)
grade: ("pi", "iop/ornith:35b", True)
for grade in range(1, 7)
},
7: ("agy", "Gemini 3.6 Flash Medium", False),
8: ("agy", "Gemini 3.6 Flash Medium", False),
7: ("agy", "Gemini 3.6 Flash (Medium)", False),
8: ("agy", "Gemini 3.6 Flash (Medium)", False),
9: ("claude", "claude-opus-4-8", False),
10: ("claude", "claude-opus-4-8", False),
},
"cloud": {
**{
grade: ("claude", "sonnet", False)
for grade in range(1, 7)
grade: ("agy", "Gemini 3.6 Flash (Low)", False)
for grade in range(1, 3)
},
**{
grade: ("agy", "Gemini 3.6 Flash (Medium)", False)
for grade in range(3, 5)
},
**{
grade: ("agy", "Gemini 3.6 Flash (High)", False)
for grade in range(5, 7)
},
7: ("claude", "claude-opus-4-8", False),
8: ("claude", "claude-opus-4-8", False),
@ -95,7 +103,7 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
self.assertEqual(decision.rule_id, "official-review-codex")
self.assertEqual(decision.candidates, (policy.CODEX_SOL_XHIGH,))
def test_dynamic_route_orders_only_the_two_allowed_candidates(self):
def test_local_g07_g08_has_one_gemini_candidate(self):
daytime = policy.select_policy(
stage="worker",
lane="local",
@ -110,11 +118,11 @@ class ExecutionTargetPolicyTests(unittest.TestCase):
)
self.assertEqual(
[candidate.adapter for candidate in daytime.candidates],
["agy", "pi"],
["agy"],
)
self.assertEqual(
[candidate.adapter for candidate in nighttime.candidates],
["pi", "agy"],
["agy"],
)
def test_invalid_inputs_are_rejected(self):

View file

@ -238,11 +238,11 @@ class SelectorContractTests(unittest.TestCase):
task_file, evaluated_at=kst(12)
)
self.assertEqual(daytime["selected"]["adapter"], "agy")
# A fresh night initial would flip to Laguna; resume must not.
# The route is time-independent; resume still pins the prior decision.
night_initial = selector.select_execution_target(
task_file, evaluated_at=kst(2)
)
self.assertEqual(night_initial["selected"]["adapter"], "pi")
self.assertEqual(night_initial["selected"], daytime["selected"])
resumed = selector.select_execution_target(
task_file,
evaluated_at=kst(2),
@ -254,7 +254,7 @@ class SelectorContractTests(unittest.TestCase):
self.assertEqual(resumed["transition"]["trigger"], "resume")
self.assertEqual(
resumed["transition"]["previous_target"],
{"adapter": "agy", "target": "Gemini 3.6 Flash Medium"},
{"adapter": "agy", "target": "Gemini 3.6 Flash (Medium)"},
)
def test_resume_requires_matching_prior_decision(self):
@ -313,12 +313,12 @@ class SelectorContractTests(unittest.TestCase):
class SelectorRouteMatrixTests(unittest.TestCase):
def test_kst_boundary_routes_through_selector(self):
def test_local_g07_g08_are_time_independent(self):
cases = [
(kst(6, 59, 59), "pi", "iop/laguna-s:2.1"),
(kst(7, 0, 0), "agy", "Gemini 3.6 Flash Medium"),
(kst(22, 59, 59), "agy", "Gemini 3.6 Flash Medium"),
(kst(23, 0, 0), "pi", "iop/laguna-s:2.1"),
(kst(6, 59, 59), "agy", "Gemini 3.6 Flash (Medium)"),
(kst(7, 0, 0), "agy", "Gemini 3.6 Flash (Medium)"),
(kst(22, 59, 59), "agy", "Gemini 3.6 Flash (Medium)"),
(kst(23, 0, 0), "agy", "Gemini 3.6 Flash (Medium)"),
]
with TemporaryDirectory() as tmp:
for grade in (7, 8):
@ -334,16 +334,20 @@ class SelectorRouteMatrixTests(unittest.TestCase):
def test_worker_route_matrix_through_selector(self):
expected = {
"local": {
**{g: ("pi", "iop/ornith-fast", "local_model", True)
**{g: ("pi", "iop/ornith:35b", "local_model", True)
for g in range(1, 7)},
7: ("agy", "Gemini 3.6 Flash Medium", "cloud_model", False),
8: ("agy", "Gemini 3.6 Flash Medium", "cloud_model", False),
7: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False),
8: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False),
9: ("claude", "claude-opus-4-8", "cloud_model", False),
10: ("claude", "claude-opus-4-8", "cloud_model", False),
},
"cloud": {
**{g: ("claude", "sonnet", "cloud_model", False)
for g in range(1, 7)},
1: ("agy", "Gemini 3.6 Flash (Low)", "cloud_model", False),
2: ("agy", "Gemini 3.6 Flash (Low)", "cloud_model", False),
3: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False),
4: ("agy", "Gemini 3.6 Flash (Medium)", "cloud_model", False),
5: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False),
6: ("agy", "Gemini 3.6 Flash (High)", "cloud_model", False),
7: ("claude", "claude-opus-4-8", "cloud_model", False),
8: ("claude", "claude-opus-4-8", "cloud_model", False),
9: ("codex", "gpt-5.6-sol", "cloud_model", False),
@ -391,7 +395,7 @@ class SelectorRouteMatrixTests(unittest.TestCase):
("codex", "gpt-5.6-sol", "cloud_model", False),
)
def test_candidate_rank_is_consecutive_and_time_ordered(self):
def test_candidate_rank_is_single_and_time_independent(self):
with TemporaryDirectory() as tmp:
dynamic = write_task_file(Path(tmp), "PLAN", "local", 8)
daytime = selector.select_execution_target(
@ -401,13 +405,13 @@ class SelectorRouteMatrixTests(unittest.TestCase):
dynamic, evaluated_at=kst(2)
)["candidates"]
self.assertEqual(
[c["candidate_rank"] for c in daytime], [1, 2]
[c["candidate_rank"] for c in daytime], [1]
)
self.assertEqual(
[c["adapter"] for c in daytime], ["agy", "pi"]
[c["adapter"] for c in daytime], ["agy"]
)
self.assertEqual(
[c["adapter"] for c in nighttime], ["pi", "agy"]
[c["adapter"] for c in nighttime], ["agy"]
)
single = write_task_file(Path(tmp), "PLAN", "cloud", 5)
candidates = selector.select_execution_target(
@ -470,7 +474,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase):
"eligible",
)
def test_exhausted_primary_selects_eligible_alternate(self):
def test_exhausted_gemini_has_no_laguna_alternate(self):
snapshot = {
"snapshot_id": "gemini-exhausted",
"source": "iop-node quota-probe",
@ -478,27 +482,17 @@ class SelectorQuotaRepresentationTests(unittest.TestCase):
"targets": [
{
"adapter": "agy",
"target": "Gemini 3.6 Flash Medium",
"target": "Gemini 3.6 Flash (Medium)",
"status": "exhausted",
}
],
}
with TemporaryDirectory() as tmp:
task_file = write_task_file(Path(tmp), "PLAN", "local", 7)
result = selector.select_execution_target(
task_file, evaluated_at=kst(12), quota_snapshot=snapshot
)
self.assertEqual(result["candidates"][0]["eligibility"], "ineligible")
self.assertEqual(
result["selected"],
{
"adapter": "pi",
"target": "iop/laguna-s:2.1",
"execution_class": "local_model",
"selfcheck_required": True,
},
)
self.assertEqual(result["quota"]["status"], "not_applicable")
with self.assertRaises(selector.SelectorInputError) as ctx:
selector.select_execution_target(
task_file, evaluated_at=kst(12), quota_snapshot=snapshot
)
def test_all_candidates_exhausted_returns_no_eligible_target(self):
snapshot = {
@ -625,15 +619,14 @@ class SelectorQuotaRepresentationTests(unittest.TestCase):
self.assertEqual(local_result["quota"]["mode"], "unbounded")
self.assertEqual(local_result["quota"]["status"], "not_applicable")
# Dynamic local G07 daytime: cloud Gemini + local Laguna candidates.
# Local G07 has one bounded Gemini candidate and no Laguna fallback.
dynamic = write_task_file(Path(tmp), "PLAN", "local", 7)
candidates = selector.select_execution_target(
dynamic, evaluated_at=kst(12)
)["candidates"]
gemini = next(c for c in candidates if c["adapter"] == "agy")
laguna = next(c for c in candidates if c["adapter"] == "pi")
self.assertEqual(gemini["quota_status"], "unknown")
self.assertEqual(laguna["quota_status"], "not_applicable")
self.assertEqual(len(candidates), 1)
self.assertEqual(candidates[0]["adapter"], "agy")
self.assertEqual(candidates[0]["quota_status"], "unknown")
def test_injected_snapshot_is_reflected(self):
snapshot = {
@ -641,7 +634,7 @@ class SelectorQuotaRepresentationTests(unittest.TestCase):
"source": "usage-checker",
"checked_at": "2026-07-25T03:00:00+09:00",
"targets": [
{"adapter": "claude", "target": "sonnet", "status": "available"}
{"adapter": "agy", "target": "Gemini 3.6 Flash (High)", "status": "available"}
],
}
with TemporaryDirectory() as tmp:

View file

@ -34,11 +34,11 @@
## 범위
- task file 경로를 필수 입력으로 받고 worker/review stage를 판별해 정규화된 JSON으로 `adapter + target`, 후보 rank, 선택 근거, quota snapshot, selfcheck 여부, 평가 시각과 적용 rule을 반환하는 결정적 selector
- KST `[07:00, 23:00)` 주간과 `[23:00, 07:00)` 야간을 적용한 `local-G07~G08` Gemini 3.6 Flash Medium / `iop/laguna-s:2.1` 선택
- 기존 정적 lane/G route를 유지하면서 `cloud-G01~G06`Sonnet으로, `cloud-G07~G08`을 Opus로, `cloud-G09~G10`을 Codex로 선택하는 실행 정책
- `local-G01~G06``iop/ornith:35b`, `local-G07~G08`은 시간대와 무관하게 Gemini 3.6 Flash (Medium)으로 선택하는 실행 정책
- 기존 정적 lane/G route를 유지하면서 `cloud-G01~G06`Gemini 3.6 Flash Low/Medium/High로, `cloud-G07~G08`을 Opus로, `cloud-G09~G10`을 Codex로 선택하는 실행 정책
- 모든 공식 `CODE_REVIEW-*`를 Codex `gpt-5.6-sol` xhigh로 고정하고 worker target 선택과 분리하는 실행 정책
- 기존 Go CLI usage checker를 중복 파싱하지 않고 좁은 JSON probe로 연결한 cloud quota의 `available | exhausted | unknown` 정규화와 local target의 `unbounded / not_applicable` 처리
- `task/plan/tag` 세대 단위 route 고정, dispatcher 재시작 후 재개, adapter 전환 시 논리적 컨텍스트 이전, 동일 stage에서 target 전환 전후에 공유하는 자동 실패 예산 10회
- `task/plan/tag` 세대 단위 route 고정, dispatcher 재시작 후 재개, 확정된 provider target 불가 때만 adapter 전환하는 논리적 컨텍스트 이전, 동일 stage에서 target 전환 전후에 공유하는 자동 실패 예산 10회
- 사용자 리뷰와 반복 실패 또는 eligible target 부재로 중단된 task 및 그 의존 task만 차단하고 독립적인 dependency-ready task는 끝까지 실행하는 drain 정책
- 실제 완료 target이 local model일 때만 selfcheck를 실행하고 cloud model 완료에는 selfcheck를 추가하지 않는 정책
- target별 인위적인 동시 실행 상한을 두지 않고 dependency-ready task를 처리량 우선으로 dispatch하는 정책
@ -50,8 +50,8 @@
정적 task route와 실행 시점 상태를 재현 가능한 `adapter + target` 결정으로 변환하는 capability를 묶는다.
- [x] [selector-contract] task file을 필수 입력으로 받는 별도 selector가 worker/review stage, lane/grade, 선택 target, candidate rank, reason code, quota snapshot, selfcheck와 `evaluated_at`을 안정적인 JSON 계약으로 반환한다. 검증: 동일한 task 세대·시간 구간·quota snapshot·이전 결정은 동일한 route 결과를 내고 invalid filename/stage는 명시적 입력 오류로 종료한다.
- [x] [time-route] KST 주간 `[07:00, 23:00)`에는 `local-G07~G08`을 Gemini 3.6 Flash Medium으로, 야간 `[23:00, 07:00)`에는 Pi `iop/laguna-s:2.1`로 선택한다. 검증: `06:59:59`, `07:00:00`, `22:59:59`, `23:00:00` 경계 테스트가 기대 route를 반환한다.
- [x] [grade-route] 기존 local grade route를 보존하고 cloud의 기존 Gemini 구간인 `G01~G06`은 Claude `sonnet`, `G07~G08`은 Opus, `G09~G10`은 Codex로 선택하며 모든 공식 CODE_REVIEW는 Codex `gpt-5.6-sol` xhigh로 고정한다. 검증: worker local/cloud `G01~G10`과 CODE_REVIEW route matrix가 빈 구간 없이 canonical adapter/target으로 해석된다.
- [x] [time-route] `local-G07~G08`을 시간대와 무관하게 Gemini 3.6 Flash (Medium) 단일 target으로 선택하고 Laguna fallback을 두지 않는다. 검증: `06:59:59`, `07:00:00`, `22:59:59`, `23:00:00` 경계 테스트가 모두 같은 route를 반환한다.
- [x] [grade-route] `local-G01~G06`은 Pi `iop/ornith:35b`, `local-G07~G08`은 Gemini 3.6 Flash (Medium), cloud `G01~G06`은 Gemini 3.6 Flash Low/Medium/High, `G07~G08`은 Opus, `G09~G10`은 Codex로 선택하며 모든 공식 CODE_REVIEW는 Codex `gpt-5.6-sol` xhigh로 고정한다. 검증: worker local/cloud `G01~G10`과 CODE_REVIEW route matrix가 빈 구간 없이 canonical adapter/target으로 해석된다.
- [x] [quota-input] 좁은 JSON quota probe가 기존 Go usage checker의 overall/model-specific remaining 신호를 재사용해 cloud quota를 `available | exhausted | unknown`으로 정규화하고, local target은 `quota_mode=unbounded`, `quota_status=not_applicable`로 표현한다. 검증: target/profile의 required cap 중 하나라도 0이면 exhausted, required cap이 모두 확인되고 양수이면 available, 누락·충돌·parse 실패는 unknown이며 unknown 후보는 각 work unit에서 runtime quota 결과가 확정될 때까지 1회만 admission하되 target 전역 cap을 만들지 않고 generic stderr만으로 quota/provider 장애를 추정하지 않는다.
### Epic: [continuation] 고정·Failover·중단 전파
@ -59,8 +59,8 @@
시간 경계나 dispatcher 재시작에도 작업 identity를 유지하고, 실패가 전체 작업 그래프를 불필요하게 멈추지 않게 한다.
- [ ] [route-pin] 최초 선택 결과를 PLAN의 `task/plan/tag` 세대에 고정·보존하고 시간 경계, PLAN 본문 갱신 또는 dispatcher 재시작만으로 활성/재개 작업의 target을 바꾸지 않는다. 검증: 주야간 경계를 넘긴 재개와 재시작이 persisted route를 유지하며 명시적 failover event만 target을 변경하고 새 plan/tag 세대만 새 최초 선택을 만든다.
- [ ] [context-failover] 주간 Gemini 첫 실행 실패는 Laguna로, 야간 Laguna 첫 실행 실패는 quota가 남은 Gemini Medium으로 단방향 논리적 컨텍스트를 이전해 계속 실행하고, 야간 Gemini quota가 없으면 해당 task를 중단한다. 검증: PLAN/locator, 이전 normalized output·raw log, 현재 workspace가 다음 adapter 입력에 연결되고 same-Pi session만 native resume을 사용하며 시간·quota 회복만으로 이전 target에 bounce하지 않는다.
- [ ] [failure-budget] 자동 복구 실패 예산 10회를 같은 work unit의 동일 stage에서 target 전환 전후에 공유하고, provider/SSE/Pi 오류 한 건만으로 즉시 전체 루프를 차단하지 않는다. 검증: target 전환 후에도 해당 stage의 누적 실패 횟수가 보존되고 stage 성공 시에만 초기화되며 10회 도달 또는 eligible target 부재 시 해당 task가 terminal blocker가 된다.
- [ ] [context-failover] 확정된 provider quota/rate-limit, context/output limit, model unavailable 또는 transport disconnect로 현재 target을 계속 쓸 수 없을 때만 주간 Gemini는 Laguna로, 야간 Laguna는 quota가 남은 Gemini Medium으로 한 번 논리적 컨텍스트를 이전한다. 그 밖의 실패는 같은 target에서 복구하고, 야간 Gemini quota가 없으면 해당 task를 중단한다. 검증: PLAN/locator, 이전 normalized output·raw log, 현재 workspace가 다음 adapter 입력에 연결되고 same-Pi session만 native resume을 사용하며 시간·quota 회복이나 실패 횟수만으로 이전 target에 bounce하거나 cloud로 승격하지 않는다.
- [ ] [failure-budget] 자동 복구 실패 예산 10회를 같은 work unit의 동일 stage에서 target 전환 전후에 공유하되, 실패 횟수는 target 전환 또는 cloud 승격 조건으로 쓰지 않는다. 검증: target 전환 후에도 해당 stage의 누적 실패 횟수가 보존되고 stage 성공 시에만 초기화되며 10회 도달 또는 eligible target 부재 시 해당 task가 terminal blocker가 된다.
- [ ] [dependency-drain] user review가 해당 task의 진행을 실제로 막는 경우와 terminal blocker가 된 경우에만 그 task 및 의존 task를 중단하고, 독립적인 dependency-ready task는 완료될 때까지 dispatch한다. 검증: 병렬 DAG에서 한 branch의 review/blocker가 다른 독립 branch의 완료를 막지 않는다.
### Epic: [integration] Dispatcher와 검증 통합
@ -101,7 +101,7 @@ selector 결정을 오케스트레이션 루프의 단일 실행 기준으로
- 표준선(선택): route는 시간 단위가 아니라 work unit 단위로 고정한다. 시간은 최초 선택 입력일 뿐이며 활성 작업의 변경 사유가 아니다.
- 표준선(선택): work unit의 selector identity는 PLAN의 `task/plan/tag` 세대이며 PLAN 본문 변경은 새 선택을 만들지 않는다. worker, selfcheck, review는 실패 예산을 공유하지 않는 별도 stage다.
- 표준선(선택): cross-adapter continuation은 PLAN/locator, 이전 출력과 로그, 현재 workspace를 이용한 logical context transfer이며 native session transfer로 표현하지 않는다.
- 표준선(선택): local quota가 무제한이라는 뜻은 quota admission을 하지 않는다는 뜻이며, 자동 실패 예산 10회와 terminal blocker 규칙은 그대로 적용한다.
- 표준선(선택): local quota가 무제한이라는 뜻은 quota admission을 하지 않는다는 뜻이며, 자동 실패 예산 10회와 terminal blocker 규칙은 그대로 적용한다. 실패 횟수는 target 전환 또는 cloud 승격 근거가 아니다.
- 큐 배치: 현재 진행 작업으로 전역 실행 순서 첫 항목
- 선행 작업: `finalize-task-routing`이 생성하는 정적 PLAN/CODE_REVIEW lane/G 파일명 계약
- 후속 작업: [CLI Agent 사용량 알림과 자동 이어받기 MVP](cli-agent-usage-notification-continuation.md)의 사용자 알림 표면

View file

@ -34,7 +34,7 @@
| 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 | 없음 | 시간, route, failover, selfcheck, 실패 예산, 동시성 정책이 확정됨 |
| User Decision | 기존 target matrix와 failover trigger 보정 | 반복·review 횟수나 일반 실패로 cloud 승격하지 않는다. failover는 확정된 provider target 불가에만 허용한다. |
## State Machine
@ -43,14 +43,14 @@
| `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 또는 아직 사용하지 않은 eligible failover가 있음 | `executing` 또는 `failover_selected` | stage별 누적 failure count, reason code, quota snapshot |
| `failover_selected` | primary target의 첫 실행 실패 뒤 정책이 단방향 다음 target을 선택하고 logical context package를 준비함 | `executing` | 이전 decision, PLAN/locator, normalized output·raw log, workspace; 이전 target으로 bounce하지 않음 |
| `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를 만들거나 이미 사용한 이전 target으로 bounce하지 않는다.
- `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
@ -86,19 +86,20 @@
- `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`ordered candidate에서 아직 사용하지 않은 다음 target으로만 이동한다. 시간대 또는 quota 회복만으로 이전 target에 bounce하지 않는다.
- `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-fast`, local selfcheck
- `local-G07~G08`, KST `[07:00, 23:00)`: `agy + Gemini 3.6 Flash Medium`; 실패 시 `pi + iop/laguna-s:2.1`로 logical context transfer 후 계속 실행
- `local-G07~G08`, KST `[23:00, 07:00)`: `pi + iop/laguna-s:2.1`; 실패 시 Gemini quota가 `available` 또는 `unknown`이면 `agy + Gemini 3.6 Flash Medium`, confirmed `exhausted`이면 해당 task 차단
- `local-G01~G06`: `pi + iop/ornith:35b`, local selfcheck
- `local-G07~G08`: `agy + Gemini 3.6 Flash (Medium)`, cloud completion이므로 selfcheck 없음
- `local-G09~G10`: `claude + claude-opus-4-8`, cloud completion이므로 selfcheck 없음
- `cloud-G01~G06`: `claude + sonnet` target. adapter가 호출 시 canonical concrete target을 기록함
- `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 기반 재선택을 적용하지 않음
- dynamic `local-G07~G08` failover는 primary에서 alternate로 한 번만 이동한다. alternate 실패는 같은 target에서 남은 동일-stage 예산으로 복구하며 primary로 돌아가지 않음
- `local-G07~G08`에는 Laguna alternate가 없으며 Gemini target이 confirmed `exhausted` 또는 불가이면 해당 task만 차단함
- 그 밖의 cloud worker는 기존 promotable failure chain을 유지하되 같은 stage의 10회 예산을 공유함
- cloud completion은 selfcheck 없음. local completion만 selfcheck를 실행함
- target별 정책 semaphore는 두지 않으며 dependency-ready task를 모두 dispatch함
@ -112,8 +113,8 @@
| 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 실패 | failover를 평가함 | 주간은 Laguna로, 야간은 quota가 있는 Gemini로 한 번만 이어지고 quota가 없으면 해당 task만 차단되며 이전 target으로 bounce하지 않음 |
| S07 | `failure-budget` | 같은 work unit의 동일 stage에서 target 전환을 포함한 연속 실패 | 자동 복구를 반복함 | stage budget이 target 간 공유되고 다른 stage와는 분리되며 10회 전에는 단일 Pi/provider/SSE 오류로 전체 루프가 차단되지 않고 stage 성공 시 초기화됨 |
| 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 없이 완료됨 |
@ -151,6 +152,7 @@
## 사용자 리뷰 이력
- 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 불가에만 허용하도록 보정했다.
## 작업 컨텍스트

View file

@ -0,0 +1,163 @@
<!-- task=m-agent-task-runtime-target-selector/03+01,02_route_pin_state plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-07-25
task=m-agent-task-runtime-target-selector/03+01,02_route_pin_state, plan=0, tag=REFACTOR
## 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를 탐색하지 않는다.
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_0.log`, `PLAN-local-G07.md``plan_local_G07_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/03+01,02_route_pin_state/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| REFACTOR-1 Selector bridge와 canonical AgentSpec | [ ] |
| REFACTOR-2 세대별 persisted decision 상태 | [ ] |
| REFACTOR-3 Decision state 회귀 고정 | [ ] |
## 구현 체크리스트
- [ ] 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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_0.log`로 아카이브한다.
- [ ] active `PLAN-local-G07.md``plan_local_G07_0.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/03+01,02_route_pin_state/``agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/03+01,02_route_pin_state/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분과 이유를 기록한다._
## 주요 설계 결정
_구현 에이전트가 source별 target namespace와 compatibility 근거를 기록한다._
## 리뷰어를 위한 체크포인트
- Pi canonical target은 decision에 보존되고 CLI model 변환에서 검증된 `iop/`만 제거되는가.
- malformed/no-eligible decision이 해당 task만 차단하고 독립 branch를 계속 drain하는가.
- body-only edit와 plan/tag generation 변경을 구분하며 live invocation/failover 범위로 확장하지 않았는가.
## 검증 결과
### RouteDecisionPersistenceTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py RouteDecisionPersistenceTest -v`
_실제 stdout/stderr와 exit code를 기록한다._
```text
_구현 에이전트가 실제 stdout/stderr와 exit code를 기록한다._
```
### Selector/policy 집중 suite
명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*execution_target*.py'`
_실제 stdout/stderr와 exit code를 기록한다._
```text
_구현 에이전트가 실제 stdout/stderr와 exit code를 기록한다._
```
### 전체 Python suite
명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'`
_실제 stdout/stderr와 exit code를 기록한다._
```text
_구현 에이전트가 실제 stdout/stderr와 exit code를 기록한다._
```
### py_compile
명령: `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`
_실제 stdout/stderr와 exit code를 기록한다._
```text
_구현 에이전트가 실제 stdout/stderr와 exit code를 기록한다._
```
### diff check
명령: `git diff --check`
_실제 stdout/stderr와 exit code를 기록한다._
```text
_구현 에이전트가 실제 stdout/stderr와 exit code를 기록한다._
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

View file

@ -0,0 +1,186 @@
<!-- 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,132 @@
<!-- task=m-agent-task-runtime-target-selector/04+03_failover_budget plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-07-25
task=m-agent-task-runtime-target-selector/04+03_failover_budget, plan=0, tag=API
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G09.md``code_review_cloud_G09_0.log`, `PLAN-local-G08.md``plan_local_G08_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/04+03_failover_budget/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| API-1 Failover selector transition | [ ] |
| API-2 논리적 context package | [ ] |
| API-3 Stage 공통 실패 예산 | [ ] |
## 구현 체크리스트
- [ ] selector가 확정된 provider target 불가에만 주야간 initial target별 단방향 failover를 반환하고, 야간 Gemini `available|unknown`은 1회 허용하며 confirmed `exhausted`만 terminal로 분류한다.
- [ ] adapter 전환용 logical context package가 PLAN/locator, persisted normalized output, raw log, workspace를 보존하고 same-Pi만 native resume으로 구분한다.
- [ ] 동일 work unit/stage 실패 예산 10회를 target 전환과 restart 전후에 공유하고 성공 때만 reset하며 `retry_blocked`는 budget/blocker만 초기화하고 decision/history는 보존한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-cloud-G09.md``code_review_cloud_G09_0.log`로 아카이브한다.
- [ ] active `PLAN-local-G08.md``plan_local_G08_0.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/04+03_failover_budget/``agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/04+03_failover_budget/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- quota `unknown`을 exhausted와 같이 막지 않고 work-unit별 한 번 허용하는가.
- normalized output artifact가 locator에 보존되고 cross-adapter에 native session을 전달하지 않는가.
- qualified provider target 불가만 failover count 1을 만들고, 일반 실패는 same-target recovery로 남으며 alternate 추가 실패와 stage budget 10회를 공유하는가.
- retry-blocked가 route history/used candidates를 지우지 않는가.
## 검증 결과
### SelectorFailoverContractTests
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests -v`
_실제 stdout/stderr와 exit code를 기록한다._
### DynamicFailoverBudgetTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest -v`
_실제 stdout/stderr와 exit code를 기록한다._
### 전체 Python suite
명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'`
_실제 stdout/stderr와 exit code를 기록한다._
### py_compile
명령: `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`
_실제 stdout/stderr와 exit code를 기록한다._
### diff check
명령: `git diff --check`
_실제 stdout/stderr와 exit code를 기록한다._
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

View file

@ -0,0 +1,193 @@
<!-- task=m-agent-task-runtime-target-selector/04+03_failover_budget plan=0 tag=API -->
# Plan - Selector failover와 공통 실패 예산
## 이 파일을 읽는 구현 에이전트에게
선행 `03+01,02_route_pin_state``complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 작성은 하지 않는다.
## 배경
selector의 `failover` transition은 현재 명시적으로 거부되고 dispatcher의 복구 예산은 정적 target 흐름에 묶여 있다. 이 작업은 live invocation을 바꾸기 전에 failover 결정 계약, 논리적 context package, 동일 work unit/stage의 공유 실패 예산을 독립적으로 완성한다.
## 분석 결과
### 읽은 파일
- `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/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`
- `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: S06 → `context-failover`, S07 → `failure-budget`.
- Evidence Map S06 행의 주야간 failover/logical context transition evidence와 S07 행의 target 전환·stage 분리·성공 초기화를 포함한 10회 counter evidence를 구현 체크리스트와 두 집중 테스트에 반영했다.
- live dispatcher 전환 없이 기반만 구현하므로 `Roadmap Targets`는 두지 않는다. 두 Task의 완료 소유자는 `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와 실 quota 호출 없이 고정 quota snapshot과 임시 locator/state를 사용한다.
- 계획 시점 기준 전체 Python suite 169 tests가 PASS다.
### 테스트 커버리지 공백
- `select_execution_target.py:592-597``failover``unsupported_transition`으로 종료한다.
- day Gemini→Laguna, night Laguna→Gemini, night quota `available|unknown|exhausted`의 단방향 결과 테스트가 없다.
- target 전환 전후 누적 실패 10회, stage 성공 시 reset, restart 보존을 한 흐름으로 검증하지 않는다.
- cross-adapter continuation payload에 PLAN/locator, normalized/raw output, workspace가 모두 포함되는지 검증하지 않는다.
### 심볼 참조
- rename/remove 없음.
- 신규 failover 계약은 selector CLI와 후속 dispatcher integration의 경계이므로 JSON 필드를 먼저 고정한다.
### 분할 판단
- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`이고 topology는 `03 → 04 → 05 → 06 → 07 → 08`이다.
- 현재 `04+03_failover_budget`의 predecessor `03+01,02_route_pin_state`는 active plan만 있고 `complete.log`가 없어 미충족이다.
- 후속 `05+04_dispatch_integration`이 이 계약을 live invocation에 연결하고 selfcheck, throughput, audit는 각각 `06`, `07`, `08`로 분리한다.
### 범위 결정 근거
- dispatcher의 정적 `route_agent()` call-site 교체는 제외한다.
- provider 오류 분류 전체 재설계와 quota producer 변경은 제외한다.
- selector는 아직 사용하지 않은 ordered candidate만 반환한다. `05`가 확정된 provider target 불가 trigger만 failover로 연결하며, 일반 실패·반복 횟수·시간 변화는 same-target recovery 또는 pin 유지로 처리한다.
- `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=`temporal_state`, `boundary_contract`, `variant_product`; 이 기록은 lane/G를 바꾸지 않는다.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/1/2`, final=`local-G08`, filename=`PLAN-local-G08.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/2/2`, final=`cloud-G09`, filename=`CODE_REVIEW-cloud-G09.md`.
## 구현 체크리스트
- [ ] selector가 확정된 provider target 불가에만 주야간 initial target별 단방향 failover를 반환하고, 야간 Gemini `available|unknown`은 1회 허용하며 confirmed `exhausted`만 terminal로 분류한다.
- [ ] adapter 전환용 logical context package가 PLAN/locator, persisted normalized output, raw log, workspace를 보존하고 same-Pi만 native resume으로 구분한다.
- [ ] 동일 work unit/stage 실패 예산 10회를 target 전환과 restart 전후에 공유하고 성공 때만 reset하며 `retry_blocked`는 budget/blocker만 초기화하고 decision/history는 보존한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 구현 항목
### [API-1] Failover selector transition
- 문제: `select_execution_target.py:592-597`은 모든 failover를 거부해 S06의 단방향 target 전환을 표현할 수 없다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:592
if transition == "failover":
raise SelectorInputError("unsupported_transition", ...)
```
```python
# After
if transition == "failover":
return _failover(
prior_decision,
evaluated_at=evaluated_at,
quota_snapshot=quota_snapshot,
)
```
prior identity/stage와 이미 사용한 candidate를 재검증하고 `previous_target`, `next_target`, qualified trigger, `context_transfer=logical`, reason code를 반환한다. quota/rate-limit, context/output limit, model unavailable 또는 transport disconnect만 qualified trigger다. 주간 Gemini는 그때만 Laguna로 이동한다. 야간 Laguna는 Gemini Medium quota가 `available` 또는 `unknown`이면 아직 사용하지 않은 alternate로 1회 이동하고, `unknown` candidate의 runtime failure가 `provider-quota`로 확인될 때만 snapshot을 `exhausted`로 갱신한다. 일반 실패, confirmed `exhausted`, candidate 소진 또는 primary bounce는 failover가 아니라 same-target recovery 또는 명시적 terminal reason으로 끝낸다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`: failover validator와 결정 함수.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: `SelectorFailoverContractTests`.
- 테스트 작성: `test_select_execution_target.py``SelectorFailoverContractTests`에서 prior initial route, available/unknown/exhausted snapshot, unknown 1회 후 confirmed provider-quota의 task-local exhausted 전환, generic stderr의 unknown 유지, used-candidate history와 no-bounce를 table fixture로 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests -v` — 정상·경계 case가 모두 PASS해야 한다.
### [API-2] 논리적 context package
- 문제: `dispatch.py:2318-2493`은 raw `stream.log`를 쓰지만 `render_json_line()`의 normalized output은 콘솔에만 출력하고, `dispatch.py:2658-2687`의 continuation은 cross-adapter 입력을 검증된 객체로 묶지 않는다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:2460
stream_log.write(f"[{channel}] {line}\n")
rendered, discovered = render_json_line(spec.cli, line)
```
```python
# After
context_package = {
"plan": ...,
"locator": ...,
"normalized_output": ...,
"raw_log": ...,
"workspace": ...,
"resume_mode": "native" | "logical",
}
```
`invoke()`가 rendered normalized lines를 attempt-local `normalized-output.log`에 순서대로 저장하고 locator에 그 경로를 기록한다. package builder는 plan/work unit/workspace, locator, normalized artifact, raw stream을 검증하며 cross-adapter에서는 native session을 전달하지 않는다. 이 subtask에서는 artifact 생성과 builder/validator까지 만들고 invocation 소비는 후속으로 남긴다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: normalized output artifact와 locator field, context package builder/validator.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: logical/native 경계 회귀.
- 테스트 작성: `test_dispatch.py``DynamicFailoverBudgetTest`에서 fake provider JSON stream으로 normalized artifact를 만들고 누락 locator/log, workspace mismatch, cross-adapter native-session 유출을 fail-closed 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest -v` — artifact/context/budget case가 모두 PASS해야 한다.
### [API-3] Stage 공통 실패 예산
- 문제: `dispatch.py:2708-2713`은 role별 counter를 읽고 `dispatch.py:2823-2828`은 성공 시 지우지만 selector target 전환 및 `clear_blocked()`(`dispatch.py:541-554`)의 decision/history 보존 계약이 없다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:2710
persisted = store.task_state(task).get("recovery_failures", {})
recovery_failures = int(persisted.get(role, 0))
```
```python
# After
budget = stage_budget(store, work_unit_id=decision["work_unit_id"], stage=stage)
budget.record_failure(target=decision["selected"], transition=transition)
```
qualified primary failure만 count 1과 failover history를 만들고 alternate의 추가 9번째 실패가 총 10 blocker가 된다. 일반 primary 실패는 same-target recovery count만 늘린다. 성공에서만 해당 stage count를 0으로 만든다. `retry_blocked`는 blocker와 stage counters만 초기화하고 persisted decision/history/used candidates는 유지한다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: budget read/increment/reset와 transition evidence helper.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: `DynamicFailoverBudgetTest`.
- 테스트 작성: `test_dispatch.py``DynamicFailoverBudgetTest`에서 qualified primary failure→alternate→추가 9회=10 blocker, generic failure same-target recovery, restart 보존, worker 성공 reset, review stage 격리, retry-blocked decision/history 보존을 temp state fixture로 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest -v` — 모든 counter/reset fixture가 PASS해야 한다.
## 의존 관계 및 구현 순서
1. `03+01,02_route_pin_state`가 archive되고 `complete.log`가 PASS일 때 시작한다. 계획 작성 시점에는 active plan만 있어 미충족이다.
2. API-1과 API-2를 구현한 뒤 API-3이 동일 identity/transition evidence를 소비하게 한다.
3. 이 task PASS 후에만 `05+04_dispatch_integration`을 실행한다.
## 수정 파일 요약
| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | API-1 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | API-2, API-3 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | API-1 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | API-2, API-3 |
## 최종 검증
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py SelectorFailoverContractTests -v` — 정상·quota·no-bounce 회귀 PASS.
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicFailoverBudgetTest -v` — context artifact·budget·retry-blocked 회귀 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/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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,153 @@
<!-- task=m-agent-task-runtime-target-selector/05+04_dispatch_integration plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-07-25
task=m-agent-task-runtime-target-selector/05+04_dispatch_integration, plan=0, tag=REFACTOR
## 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:
- `route-pin`: 작업 세대별 최초 route 고정과 재시작 복원
- `context-failover`: 단방향 adapter failover와 논리적 context 이전
- `failure-budget`: target 전환 전후 동일 stage 실패 예산 공유
- `dispatch-integration`: persisted selector decision 기반 최초 실행·resume·failover
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G09.md``code_review_cloud_G09_0.log`, `PLAN-local-G08.md``plan_local_G08_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/05+04_dispatch_integration/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| REFACTOR-1 Live route source 교체 | [ ] |
| REFACTOR-2 Runtime quota probe와 resume/failover state machine | [ ] |
| REFACTOR-3 Context, budget과 retry-blocked 소비 | [ ] |
| REFACTOR-4 Review recovery와 runtime audit evidence | [ ] |
## 구현 체크리스트
- [ ] worker/review initial invocation과 dry-run이 persisted selector decision을 단일 route source로 사용한다.
- [ ] bounded candidate의 runtime quota probe가 target/profile required-cap argv로 실행되고 probe 오류는 `unknown` snapshot으로 decision에 남는다.
- [ ] resume은 시간·본문 변경에도 pinned target을 사용하고, 확정된 provider target 불가 또는 명시적 retry eligibility만 transition history와 invocation target을 변경한다.
- [ ] cross-adapter invocation이 persisted normalized-output context package를 소비하고 dynamic failover와 기존 cloud promotion 모두 같은 stage 실패 예산을 이어받는다.
- [ ] dry-run은 read-only preview이고 retry-blocked는 blocker/budget만 초기화하며 review resume/finalization recovery는 pinned Codex와 독립 budget을 유지한다.
- [ ] locator/state/status가 work unit, rule/candidate/reason, quota, transition과 stage budget을 추적하고 통합 테스트가 restart·blocker·독립 drain을 함께 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-cloud-G09.md``code_review_cloud_G09_0.log`로 아카이브한다.
- [ ] active `PLAN-local-G08.md``plan_local_G08_0.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/05+04_dispatch_integration/``agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/05+04_dispatch_integration/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- runtime probe argv의 canonical target/profile/required caps와 error→unknown이 Go producer 계약과 일치하는가.
- dynamic primary의 확정된 provider target 불가만 failover하고, 일반 실패는 same-target recovery로 남으며 기존 cloud promotion은 history/budget에 남는가.
- retry-blocked, dry-run, finalization recovery가 generation/history를 손상하거나 quota/failover를 재평가하지 않는가.
- locator/state/status/dry-run의 selector evidence가 동일 work unit/transition을 가리키고 독립 branch drain을 보존하는가.
## 검증 결과
### SelectorDispatcherIntegrationTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v`
_실제 stdout/stderr와 exit code를 기록한다._
### Selector 전체 회귀
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v`
_실제 stdout/stderr와 exit code를 기록한다._
### BlockerDrainTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v`
_실제 stdout/stderr와 exit code를 기록한다._
### 전체 Python suite
명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'`
_실제 stdout/stderr와 exit code를 기록한다._
### py_compile
명령: `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`
_실제 stdout/stderr와 exit code를 기록한다._
### diff check
명령: `git diff --check`
_실제 stdout/stderr와 exit code를 기록한다._
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

View file

@ -0,0 +1,237 @@
<!-- task=m-agent-task-runtime-target-selector/05+04_dispatch_integration plan=0 tag=REFACTOR -->
# Plan - Dispatcher selector 단일 실행 기준 통합
## 이 파일을 읽는 구현 에이전트에게
선행 `04+03_failover_budget``complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G09.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다.
## 배경
dispatcher는 selector가 존재해도 worker route, 실행, dry-run에서 정적 `route_agent()`를 다시 사용한다. 이 작업은 앞선 persisted decision과 failover 계약을 모든 live invocation의 단일 기준으로 연결해 고정, 재개, 전환을 실제 실행 경계에서 완성한다.
## 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:
- `route-pin`: 작업 세대별 최초 route 고정과 재시작 복원
- `context-failover`: 단방향 adapter failover와 논리적 context 이전
- `failure-budget`: target 전환 전후 동일 stage 실패 예산 공유
- `dispatch-integration`: persisted selector decision 기반 최초 실행·resume·failover
- 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/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`
- `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 → `route-pin`, S06 → `context-failover`, S07 → `failure-budget`, S09 → `dispatch-integration`.
- Evidence Map S05~S07/S09 행의 persisted resume/restart, logical context transition, stage counter, worker/review initial·resume·failover invocation audit를 구현 체크리스트와 `SelectorDispatcherIntegrationTest`에 반영했다.
### 테스트 환경 규칙
- `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 subprocess는 fake runner와 임시 workspace/state/locator로 대체하고 실제 cloud/local 호출은 하지 않는다.
- 계획 시점 전체 Python suite 기준선은 169 tests PASS다.
### 테스트 커버리지 공백
- `dispatch.py:977` 정적 route, `run_worker()` 부근의 실행 spec, dry-run 출력은 selector decision을 소비하지 않는다.
- dispatcher와 standalone selector 어디에도 `iop-node quota-probe` runtime 호출부와 target/profile required-cap argv 매핑이 없다.
- initial/resume/failover 각각의 invocation target과 persisted history가 일치하는 end-to-end 테스트가 없다.
- review 고정 decision의 resume/finalization recovery와 worker 동적 decision이 분리되는지 검증하지 않는다.
- dry-run 무상태, retry-blocked eligibility 재평가, runtime locator/status의 selector audit field 회귀가 없다.
### 심볼 참조
- `route_agent()` 정의 `dispatch.py:977`과 호출부 `run_worker()`(`dispatch.py:3423`), `run_selfcheck()`(`dispatch.py:3468`), dry-run(`dispatch.py:4201`), 테스트 참조 `test_dispatch.py:275,2499`를 selector bridge로 옮긴 뒤 제거한다.
- 기존 `promoted_spec()`(`dispatch.py:1631`)과 `run_escalating()` promotion call-site(`dispatch.py:2960-2987`)는 기타 cloud worker chain으로 유지하되 transition history/budget 소비자로 갱신한다.
### 분할 판단
- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`이고 topology는 `03 → 04 → 05 → 06 → 07 → 08`이다.
- 현재 `05+04_dispatch_integration`의 predecessor `04+03_failover_budget`는 active plan만 있고 `complete.log`가 없어 미충족이다.
- `03` state와 `04` failover/budget 계약을 함께 소비한다. selfcheck와 throughput이 같은 dispatcher/test를 수정하므로 `06``07`로 직렬 분리하고 운영 audit는 `08`이 소유한다.
### 범위 결정 근거
- selector target matrix와 Go quota producer는 변경하지 않는다. selector/dispatcher에 producer의 runtime argv/profile 매핑 소비를 추가한다.
- selfcheck target 재사용과 semaphore 제거는 후속 plan 범위다.
- 사용자 리뷰/blocker dependency drain은 이미 완료된 정책을 보존하며 재설계하지 않는다.
- `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=`temporal_state`, `boundary_contract`, `variant_product`; 이 기록은 lane/G를 바꾸지 않는다.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/2/1/1/2`, final=`local-G08`, filename=`PLAN-local-G08.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/2/2`, final=`cloud-G09`, filename=`CODE_REVIEW-cloud-G09.md`.
## 구현 체크리스트
- [ ] worker/review initial invocation과 dry-run이 persisted selector decision을 단일 route source로 사용한다.
- [ ] bounded candidate의 runtime quota probe가 target/profile required-cap argv로 실행되고 probe 오류는 `unknown` snapshot으로 decision에 남는다.
- [ ] resume은 시간·본문 변경에도 pinned target을 사용하고, 확정된 provider target 불가 또는 명시적 retry eligibility만 transition history와 invocation target을 변경한다.
- [ ] cross-adapter invocation이 persisted normalized-output context package를 소비하고 dynamic failover와 기존 cloud promotion 모두 같은 stage 실패 예산을 이어받는다.
- [ ] dry-run은 read-only preview이고 retry-blocked는 blocker/budget만 초기화하며 review resume/finalization recovery는 pinned Codex와 독립 budget을 유지한다.
- [ ] locator/state/status가 work unit, rule/candidate/reason, quota, transition과 stage budget을 추적하고 통합 테스트가 restart·blocker·독립 drain을 함께 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 구현 항목
### [REFACTOR-1] Live route source 교체
- 문제: `dispatch.py:977``route_agent()`와 worker/dry-run call-site가 파일명 lane/G를 정적 CLI spec으로 다시 해석한다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:3423
agent = route_agent(task)
```
```python
# After
decision = state.resolve_execution_decision(task, stage=stage, transition=transition)
agent = agent_spec_from_decision(decision)
```
selector decision 생성과 저장을 원자적으로 연결하고 이미 저장된 stage decision은 반드시 resume으로 검증한다. `route_agent()`와 위에 열거한 source/test call-site를 제거한다. 공식 review는 selector의 review 고정 결과를 쓰고 worker decision을 재사용하지 않는다. dry-run은 persisted decision이 있으면 read-only resume preview, 없으면 non-persisted initial preview만 만들며 state/history/admission 기록을 쓰지 않는다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: live worker/review/dry-run route 소비자와 static helper 제거.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: 제거된 helper 참조 갱신과 initial/review/dry-run assertions.
- 테스트 작성: `test_dispatch.py``SelectorDispatcherIntegrationTest`에서 정적 matrix와 selector 결과가 다른 temp task, fake runner, reopened state를 사용해 worker/review initial과 dry-run 무상태를 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — initial/review/dry-run case가 PASS해야 한다.
### [REFACTOR-2] Runtime quota probe와 resume/failover state machine
- 문제: `select_execution_target.py:485-491`은 probe command를 source 문자열로만 기록하고 실제 호출하지 않으며, `dispatch.py:2690-2987` recovery는 selector transition history와 runtime quota를 하나의 state machine으로 갱신하지 않는다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py:485
"source": quota_probe_command,
```
```python
# After
snapshot = probe_candidate_quota(
target=candidate.target,
adapter=candidate.adapter,
required_caps=quota_probe_spec(candidate).required_caps,
checked_at=evaluated_at,
)
```
- bounded candidate를 실제 평가할 때 shell 없이 `iop-node quota-probe --target <canonical> --command <adapter> --required-cap ... --checked-at <RFC3339>`를 실행한다. policy-owned mapping은 Gemini 3.6 Flash Low/Medium/High에 `overall`과 해당 `model:<canonical>` cap을, Claude/Codex에 `overall`을 선언한다. local model candidate는 probe하지 않고 오류/parse failure는 같은 target의 `unknown` snapshot으로 정규화한다.
- persisted prior decision을 `resume`에 전달한다. `local-G07~G08`은 Gemini 3.6 Flash (Medium) 단일 target이므로 Laguna failover를 평가하지 않고 confirmed exhausted 또는 provider target 불가를 task-local blocker로 기록한다. 그 밖의 process/generic 오류와 dispatcher state/work-log/control/user-review/cancel 오류는 같은 target recovery로 처리한다.
- 기타 cloud worker의 기존 `agy → Claude → Codex` promotion은 보존하면서 normalized transition/history와 공통 stage budget에 기록한다.
- selector 반환, budget increment, used-candidate/history append, 다음 invocation 준비를 동일 task-state update 안에서 처리한다. unknown candidate의 confirmed runtime `provider-quota`는 해당 work unit의 derived evidence만 exhausted로 바꾸고, generic stderr는 unknown을 유지한다. invalid/no-eligible와 confirmed exhausted는 task-local blocker다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py`: canonical target별 command/profile/required-cap probe spec.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`: no-shell subprocess/normalization과 injectable snapshot 경계.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: resume/failover/promotion orchestration과 task-local terminal state.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`: target별 probe spec matrix.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: probe argv, parse/error→unknown과 bounded/local matrix.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: restart/time/failure-class/failover/promotion integration.
- 테스트 작성: `SelectorDispatcherIntegrationTest`에서 day→night pin, body edit, new generation, qualified provider target 불가 failover, generic/Pi same-target recovery, excluded control error, available/unknown 1회 Gemini, confirmed exhausted blocker, 기존 cloud promotion을 검증한다. selector test에는 fake probe argv와 snapshot을 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v``python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v`가 PASS해야 한다.
### [REFACTOR-3] Context, budget과 retry-blocked 소비
- 문제: `dispatch.py:2658-2687` continuation과 `dispatch.py:2708-2828` budget lifecycle은 선행 task의 normalized context package, selector history, `clear_blocked()` 보존 규칙을 실제 invocation에 연결하지 않는다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:2790
prompt = base_prompt(task, role, spec) if previous_locator is None else continuation_prompt(...)
```
```python
# After
prompt = continuation_prompt_from_package(
context_package,
target=decision["selected"],
native_resume=same_pi_target,
)
```
qualified primary failure는 count 1, alternate 추가 9회 실패가 총 10 blocker가 되며 성공 target을 state/locator에 기록하고 동일 stage 성공에서만 reset한다. 일반 primary 실패는 same-target count만 늘리고 전환하지 않는다. `--retry-blocked`는 blocker와 stage counter를 초기화하되 decision/history/used candidate를 보존하고, quota-exhausted alternate만 새 snapshot으로 재평가해 available/unknown이면 진행하며 시간 변화·실패 횟수로 새 initial/bounce/cloud 승격하지 않는다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: invocation payload, locator/result state, budget lifecycle.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: fake runner input과 누적 failure evidence.
- 테스트 작성: `SelectorDispatcherIntegrationTest`에서 primary 1회→alternate 9회=10 blocker, restart, 성공 reset, retry-blocked exhausted→available, history 보존, 독립 branch drain을 temp state/locator로 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — context/budget/retry/drain case가 PASS해야 한다.
### [REFACTOR-4] Review recovery와 runtime audit evidence
- 문제: `dispatch.py:794-865` finalization recovery는 active review `.md`가 없을 수 있고, locator/status/dry-run(`dispatch.py:2158-2186,3665-3672,4193-4222`)에는 selector work unit/rule/candidate/reason/quota/transition/budget이 없다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:4198
spec = AgentSpec("codex", "gpt-5.6-sol", "codex/gpt-5.6-sol xhigh") if stage == "review" else route_agent(task)
```
```python
# After
decision = read_or_preview_stage_decision(task, state, stage=stage, dry_run=args.dry_run)
lines = status_lines(task, stage, "ready") + selector_evidence_lines(decision)
```
active review initial에서 Codex decision을 stage별로 보존하고 archive recovery는 동일 header identity의 persisted decision을 resume한다. legacy state에 decision이 없으면 archived header를 검증해 official-review fixed Codex decision만 합성하며 quota probe/failover를 금지한다. locator와 persisted transition history에 `work_unit_id`, `rule_id`, priority, candidate ranks/selected target, reason codes, quota snapshot id/status, transition과 stage budget을 기록하고 status/dry-run은 secret/raw provider output 없이 같은 evidence를 표시한다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: review recovery와 locator/status/dry-run audit fields.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: active review resume, post-archive restart와 evidence consistency.
- 테스트 작성: `SelectorDispatcherIntegrationTest`에서 review pinned Codex resume/독립 budget/no quota/no worker decision reuse, legacy fixed recovery, state-locator-status-dry-run evidence 일치를 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — review recovery와 audit assertions가 PASS해야 한다.
## 의존 관계 및 구현 순서
1. `04+03_failover_budget`의 archived `complete.log`가 PASS여야 한다. 계획 작성 시점에는 아직 미충족이다.
2. REFACTOR-1 → REFACTOR-2 → REFACTOR-3 순서로 연결한다.
3. 이 task 완료 후 `06+05_selfcheck_policy`, 이어서 `07+06_throughput_policy`를 진행한다.
## 수정 파일 요약
| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py` | REFACTOR-1, REFACTOR-2, REFACTOR-3, REFACTOR-4 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | REFACTOR-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | REFACTOR-1, REFACTOR-2, REFACTOR-3, REFACTOR-4 |
## 최종 검증
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest -v` — initial/resume/failover/retry/review/audit 회귀 PASS.
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py -v` — runtime probe와 selector 전체 회귀 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/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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,134 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_selfcheck_policy plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-07-25
task=m-agent-task-runtime-target-selector/06+05_selfcheck_policy, plan=0, tag=REFACTOR
## 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:
- `selfcheck-policy`: 실제 worker 완료 target의 local/cloud 성격으로 selfcheck 결정
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G05.md``code_review_cloud_G05_0.log`, `PLAN-local-G04.md``plan_local_G04_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| REFACTOR-1 Completing decision 저장 | [ ] |
| REFACTOR-2 Selfcheck scheduling과 target 재사용 | [ ] |
## 구현 체크리스트
- [ ] worker 성공 state가 실제 completing decision, target, execution_class를 보존한다.
- [ ] selfcheck는 completing decision의 `execution_class=local_model`일 때만 정확히 한 번 scheduling된다.
- [ ] local selfcheck가 새 selector 평가 없이 completing target과 같은 adapter/target을 재사용한다.
- [ ] Gemini→Laguna, Laguna→Gemini, cloud 완료와 restart 회귀 테스트가 정책을 고정한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-cloud-G05.md``code_review_cloud_G05_0.log`로 아카이브한다.
- [ ] active `PLAN-local-G04.md``plan_local_G04_0.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/``agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/06+05_selfcheck_policy/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- completing decision과 성공 locator target이 일치하고 실제 enum `local_model|cloud_model`을 쓰는가.
- task lane/최초 target이 아니라 completing decision만 selfcheck scheduling을 결정하는가.
- selfcheck가 selector initial/failover 또는 quota probe를 호출하지 않고 pinned target을 재사용하는가.
- restart 후 selfcheck가 중복 실행되지 않고 worker와 별도 stage budget을 유지하는가.
## 검증 결과
### CompletingTargetSelfcheckTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py CompletingTargetSelfcheckTest -v`
_실제 stdout/stderr와 exit code를 기록한다._
### 전체 Python suite
명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'`
_실제 stdout/stderr와 exit code를 기록한다._
### py_compile
명령: `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`
_실제 stdout/stderr와 exit code를 기록한다._
### diff check
명령: `git diff --check`
_실제 stdout/stderr와 exit code를 기록한다._
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

View file

@ -0,0 +1,172 @@
<!-- task=m-agent-task-runtime-target-selector/06+05_selfcheck_policy plan=0 tag=REFACTOR -->
# Plan - 실제 완료 target 기반 selfcheck
## 이 파일을 읽는 구현 에이전트에게
선행 `05+04_dispatch_integration``complete.log`가 확인된 뒤 구현한다. 구현과 검증을 끝낸 뒤 `CODE_REVIEW-cloud-G05.md`의 구현 에이전트 소유 섹션을 실제 내용과 원문 출력으로 채운다. active 파일은 그대로 두고 review 준비 완료만 보고한다. 종결은 code-review skill 전용이다. 차단되면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 에이전트 소유 evidence에 기록한다. 사용자에게 질문하거나 user-input 도구를 호출하지 않고, control-plane stop file 생성, 다음 상태 분류, archive/log 이동, `complete.log` 또는 roadmap 수정은 하지 않는다.
## 배경
현재 selfcheck 여부는 task의 정적 local route 성격에 기대고 있어 failover 후 실제 완료 target과 어긋날 수 있다. 이 작업은 worker 성공을 만든 decision을 완료 evidence로 고정하고, 그 target의 local/cloud 성격만으로 selfcheck를 결정한다.
## 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:
- `selfcheck-policy`: 실제 worker 완료 target의 local/cloud 성격으로 selfcheck 결정
- 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-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: S10 → `selfcheck-policy`.
- Evidence Map S10 행의 completing-target stage evidence를 따라 Gemini→Laguna 완료 selfcheck, Laguna→Gemini/cloud 완료 미실행, pinned target 재사용을 구현 체크리스트와 집중 테스트에 반영했다.
### 테스트 환경 규칙
- `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 유지보수는 필요하지 않다.
- fake runner로 worker/selfcheck invocation spec을 포착하고 실제 provider는 호출하지 않는다.
- 계획 시점 전체 Python suite 기준선은 169 tests PASS다.
### 테스트 커버리지 공백
- dispatcher selfcheck 판단이 `completed_spec.local_pi`와 정적 route 정보에 의존한다.
- failover 뒤 실제 성공 target의 `execution_class`를 완료 state에 고정해 쓰지 않는다.
- local selfcheck가 새 initial selector decision을 만들지 않고 성공 target을 그대로 재사용하는지 검증하지 않는다.
### 심볼 참조
- `task_requires_selfcheck()` 정의 `dispatch.py:1008`과 유일 call-site `task_stage()`(`dispatch.py:1091`)를 completing decision 기반 helper로 교체한다.
- `run_worker()` 완료 기록 `dispatch.py:3450-3458``run_selfcheck()`의 정적 route call `dispatch.py:3468`을 갱신한다. `run_selfcheck` 자체는 rename/remove하지 않는다.
### 분할 판단
- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`이고 topology는 `03 → 04 → 05 → 06 → 07 → 08`이다.
- 현재 `06+05_selfcheck_policy`의 predecessor `05+04_dispatch_integration`은 active plan만 있고 `complete.log`가 없어 미충족이다.
- throughput도 같은 `dispatch.py``test_dispatch.py`를 수정하므로 이 task 뒤 `07+06_throughput_policy`로 직렬화하고 최종 audit는 `08`이 담당한다.
### 범위 결정 근거
- selector route matrix와 failover eligibility는 변경하지 않는다.
- selfcheck는 worker와 별도 stage budget/lifecycle을 유지하되 route를 새로 평가하지 않는다.
- review 실행 정책은 공식 Codex 고정을 그대로 보존한다.
- `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=`temporal_state`; 이 기록은 lane/G를 바꾸지 않는다.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`1/1/1/0/1`, final=`local-G04`, filename=`PLAN-local-G04.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/1/1/1/1`, final=`cloud-G05`, filename=`CODE_REVIEW-cloud-G05.md`.
## 구현 체크리스트
- [ ] worker 성공 state가 실제 completing decision, target, execution_class를 보존한다.
- [ ] selfcheck는 completing decision의 `execution_class=local_model`일 때만 정확히 한 번 scheduling된다.
- [ ] local selfcheck가 새 selector 평가 없이 completing target과 같은 adapter/target을 재사용한다.
- [ ] Gemini→Laguna, Laguna→Gemini, cloud 완료와 restart 회귀 테스트가 정책을 고정한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 구현 항목
### [REFACTOR-1] Completing decision 저장
- 문제: `dispatch.py:3450-3458`은 locator의 CLI/model과 `local_pi`만 기록해 failover 뒤 실제 selector completing decision을 selfcheck 판단의 authoritative evidence로 제공하지 않는다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:3450
completed_spec = agent_spec_from_locator(locator) or spec
store.update_task(
task,
worker_done=True,
worker_cli=completed_spec.cli,
worker_model=completed_spec.model,
selfcheck_done=not completed_spec.local_pi,
)
```
```python
# After
state.mark_worker_done(
task,
completing_decision=decision,
execution_class=decision["selected"]["execution_class"],
)
```
decision identity와 stage를 검증한 뒤 성공 locator와 같은 target만 기록한다. restart 시에도 이 완료 evidence를 보존한다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: worker completion state schema와 validator.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: success target/state 일치 회귀.
- 테스트 작성: `test_dispatch.py``CompletingTargetSelfcheckTest`에서 fake success locator와 persisted decision으로 initial/failover completing target 및 identity mismatch fail-closed를 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py CompletingTargetSelfcheckTest -v` — completion state case가 PASS해야 한다.
### [REFACTOR-2] Selfcheck scheduling과 target 재사용
- 문제: `dispatch.py:1008-1009,1091`은 task lane/G로 scheduling하고 `dispatch.py:3468`은 정적 route를 다시 계산해 실제 cloud 완료에 불필요한 selfcheck를 만들거나 local 완료 target을 바꿀 수 있다.
- 해결 방법:
```python
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py:1008
def task_requires_selfcheck(task: Task) -> bool:
return task.lane == "local" and task.grade is not None and task.grade <= 8
```
```python
# After
def completing_decision_requires_selfcheck(state: dict[str, Any]) -> bool:
return state["completing_decision"]["selected"]["execution_class"] == "local_model"
```
persisted completing decision에서 `AgentSpec`을 복원한다. selfcheck stage state/budget은 worker와 별도지만 selector `initial`/`failover`와 quota probe를 호출하지 않는다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: scheduling 조건과 `run_selfcheck` spec source.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: target 재사용과 no-selfcheck matrix.
- 테스트 작성: `CompletingTargetSelfcheckTest`에서 Gemini→Laguna는 Laguna selfcheck 1회, Laguna→Gemini와 모든 cloud 완료는 0회, selector/probe call 0회, restart 후 중복 실행 없음을 fake runner로 검증한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py CompletingTargetSelfcheckTest -v` — target/no-selfcheck/restart case가 PASS해야 한다.
## 의존 관계 및 구현 순서
1. `05+04_dispatch_integration`의 archived `complete.log`가 PASS여야 한다. 계획 작성 시점에는 아직 미충족이다.
2. REFACTOR-1 완료 evidence를 REFACTOR-2가 소비한다.
3. 이 task PASS 후 `07+06_throughput_policy`를 시작하며, `06``07`이 모두 PASS해야 `08+06,07_audit_closure`를 시작한다.
## 수정 파일 요약
| 파일 | 항목 |
|---|---|
| `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 |
## 최종 검증
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py CompletingTargetSelfcheckTest -v` — completion/selfcheck matrix 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,141 @@
<!-- task=m-agent-task-runtime-target-selector/07+06_throughput_policy plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-07-25
task=m-agent-task-runtime-target-selector/07+06_throughput_policy, plan=0, tag=REFACTOR
## 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
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_0.log`, `PLAN-local-G07.md``plan_local_G07_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/07+06_throughput_policy/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| REFACTOR-1 Target별 semaphore 제거 | [ ] |
| REFACTOR-2 Admission batch quota snapshot | [ ] |
| REFACTOR-3 Unknown 격리와 회귀 | [ ] |
## 구현 체크리스트
- [ ] 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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_0.log`로 아카이브한다.
- [ ] active `PLAN-local-G07.md``plan_local_G07_0.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/07+06_throughput_policy/``agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/07+06_throughput_policy/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- target/model 기반 semaphore를 제거하되 state/work-log 정합성 lock은 보존하는가.
- aggregate envelope이 unique probe key별 single-target Go result를 정확히 합치고 same batch id/time을 공유하는가.
- resume/selfcheck가 재probe하지 않고 mixed target status가 서로 오염되지 않는가.
- confirmed provider-quota derived evidence가 immutable shared envelope과 다른 work unit을 변경하지 않는가.
## 검증 결과
### ThroughputQuotaBatchTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py ThroughputQuotaBatchTest -v`
_실제 stdout/stderr와 exit code를 기록한다._
### BlockerDrainTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py BlockerDrainTest -v`
_실제 stdout/stderr와 exit code를 기록한다._
### 전체 Python suite
명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'`
_실제 stdout/stderr와 exit code를 기록한다._
### py_compile
명령: `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`
_실제 stdout/stderr와 exit code를 기록한다._
### diff check
명령: `git diff --check`
_실제 stdout/stderr와 exit code를 기록한다._
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

View file

@ -0,0 +1,199 @@
<!-- 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`의 구현 에이전트 소유 섹션을 채운다.

View file

@ -0,0 +1,141 @@
<!-- task=m-agent-task-runtime-target-selector/08+06,07_audit_closure plan=0 tag=TEST -->
# Code Review Reference - TEST
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-07-25
task=m-agent-task-runtime-target-selector/08+06,07_audit_closure, plan=0, tag=TEST
## 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:
- `audit-tests`: 선택·고정·failover·quota·예산·blocker 추적과 테스트·운영 설명 일치
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_0.log`, `PLAN-local-G07.md``plan_local_G07_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/08+06,07_audit_closure/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| TEST-1 Cross-scenario audit evidence | [ ] |
| TEST-2 운영 skill 정책 동기화 | [ ] |
| TEST-3 Drift guard와 전체 회귀 | [ ] |
## 구현 체크리스트
- [ ] 통합 audit가 work unit, rule/priority, candidate rank/selected target, reason, quota, transition으로 initial, pin/resume, failover, budget과 quota/review blocker를 추적한다.
- [ ] 운영 skill이 selector 호출 시점, target matrix, route pin, failover, selfcheck, batch quota와 무제한 target admission을 실제 구현과 일치하게 설명한다.
- [ ] deterministic drift search와 전체 suite가 구 정적 route/cap 설명 및 정책 회귀가 없음을 증명한다.
- [ ] SDD Evidence Map의 대표 scenario와 Roadmap Target 검증 근거가 review 문서에 추적 가능하게 기록된다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_0.log`로 아카이브한다.
- [ ] active `PLAN-local-G07.md``plan_local_G07_0.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-agent-task-runtime-target-selector/08+06,07_audit_closure/``agent-task/archive/YYYY/MM/m-agent-task-runtime-target-selector/08+06,07_audit_closure/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-agent-task-runtime-target-selector`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-agent-task-runtime-target-selector/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이면 code-review skill의 판정에 맞는 다음 filesystem state를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- S12의 success, qualified failover, generic same-target recovery, quota block, budget block, review block 각각에 전체 selector evidence field가 연결되는가.
- 운영 문서가 live 구현을 현재형으로 설명하고 구 Gemini/static cap/route helper를 남기지 않는가.
- audit가 문자열 검색만이 아니라 persisted state, locator, status/dry-run의 behavior를 검증하는가.
- project skill과 tests만 변경하고 새로운 정책이나 common rule을 추가하지 않았는가.
## 검증 결과
### DynamicSelectorAuditTest
명령: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicSelectorAuditTest -v`
_실제 stdout/stderr와 exit code를 기록하고 S12 다섯 outcome별 work unit/rule/candidate/reason/quota/transition evidence 위치를 함께 적는다._
### 전체 Python suite
명령: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'`
_실제 stdout/stderr와 exit code를 기록한다._
### py_compile
명령: `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`
_실제 stdout/stderr와 exit code를 기록한다._
### 정책 drift zero-match assertion
명령: `bash -c 'matches=$(rg --sort path -n '"'"'Gemini 3\\.5|동시 실행 제한|route_agent\\(|invocation_semaphore|^[[:space:]]*semaphores = \\{'"'"' agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py); rc=$?; test "$rc" -eq 1 && test -z "$matches"'`
_실제 stdout/stderr와 exit code를 기록한다. 금지 production match 0건과 exit 0이 필요하다._
### diff check
명령: `git diff --check`
_실제 stdout/stderr와 exit code를 기록한다._
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

View file

@ -0,0 +1,163 @@
<!-- task=m-agent-task-runtime-target-selector/08+06,07_audit_closure plan=0 tag=TEST -->
# Plan - 동적 selector 정책 audit와 운영 설명 동기화
## 이 파일을 읽는 구현 에이전트에게
선행 `06+05_selfcheck_policy``07+06_throughput_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 수정은 하지 않는다.
## 배경
기능 구현이 끝나도 skill의 정적 route/동시성 설명과 분산된 테스트가 새 selector 정책을 완전하게 증명하지 못하면 운영 drift가 남는다. 이 작업은 새 기능을 추가하지 않고 정책 matrix, 추적 evidence, 단위·통합 회귀, 운영 설명을 하나의 최종 audit로 닫는다.
## 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:
- `audit-tests`: 선택·고정·failover·quota·예산·blocker 추적과 테스트·운영 설명 일치
- 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/common/philosophy.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`
- `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: S12 → `audit-tests`.
- Evidence Map S12 행의 회귀 테스트, reason/transition 로그와 운영 문서 diff를 따라 success, failover, quota block, budget block, review block의 work unit/rule/candidate/reason/transition evidence를 구현 체크리스트와 최종 검증에 반영했다.
### 테스트 환경 규칙
- `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 유지보수는 필요하지 않다.
- fake clock/quota/runner와 임시 workspace를 사용해 provider 호출 없이 재현한다.
- 계획 시점 전체 Python suite 기준선은 169 tests PASS다.
### 테스트 커버리지 공백
- 단위 테스트들이 개별 selector/dispatcher 속성은 검증하지만 하나의 work unit이 initial→resume/failover→success/selfcheck 또는 blocker로 이어지는 audit chain을 고정하지 않는다.
- 운영 skill의 route 표와 처리량/자가검증 설명에는 정적 정책과 구 모델명이 남아 있어 새 runtime 기준과 drift한다.
- work unit, rule/priority, candidate rank/selected target, reason, quota, transition이 locator, persisted history, status/dry-run에 일관되게 나타나는지 한 번에 검증하지 않는다.
### 심볼 참조
- project `SKILL.md`의 route 표, 실행 절차, 승격/context 복구, selfcheck, 동시 실행 제한, 검증 체크리스트를 전수 대조한다.
- 제거가 예정된 `route_agent`, `invocation_semaphore`, target semaphore map, 구 모델명과 운영 문구를 `rg --sort path` 기반 zero-match assertion으로 감사한다.
### 분할 판단
- split 정책을 먼저 평가했다. shared task group은 `m-agent-task-runtime-target-selector`이고 topology는 `03 → 04 → 05 → 06 → 07 → 08`이다.
- 현재 `08+06,07_audit_closure`의 predecessor `06+05_selfcheck_policy``07+06_throughput_policy`는 둘 다 active plan만 있고 `complete.log`가 없어 미충족이다.
- 이 task는 `03`~`07`의 최종 cross-policy 검증/문서 closure다. 더 나누면 같은 evidence를 중복 감사하므로 추가 split하지 않는다.
### 범위 결정 근거
- 새 selector 정책, provider adapter, quota producer, roadmap 완료 리뷰는 추가/변경하지 않는다.
- audit에서 기능 결함이 발견되면 숨겨서 우회하지 않고 이 plan 범위의 직접 결함만 수정하거나 review evidence에 후속 필요를 기록한다.
- 중앙 관리 common skill/rule은 수정하지 않고 project skill만 동기화한다.
- `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=`없음`; 이 기록은 lane/G를 바꾸지 않는다.
- build scores(scope_coupling/state_concurrency/blast_irreversibility/evidence_diagnosis/verification_complexity)=`2/1/0/2/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)=`2/1/0/2/2`, final=`cloud-G07`, filename=`CODE_REVIEW-cloud-G07.md`.
## 구현 체크리스트
- [ ] 통합 audit가 work unit, rule/priority, candidate rank/selected target, reason, quota, transition으로 initial, pin/resume, failover, budget과 quota/review blocker를 추적한다.
- [ ] 운영 skill이 selector 호출 시점, target matrix, route pin, failover, selfcheck, batch quota와 무제한 target admission을 실제 구현과 일치하게 설명한다.
- [ ] deterministic drift search와 전체 suite가 구 정적 route/cap 설명 및 정책 회귀가 없음을 증명한다.
- [ ] SDD Evidence Map의 대표 scenario와 Roadmap Target 검증 근거가 review 문서에 추적 가능하게 기록된다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.
## 구현 항목
### [TEST-1] Cross-scenario audit evidence
- 문제: `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py:1-5088`의 selector 관련 회귀는 여러 클래스에 분산돼 S12가 요구하는 success/failover/quota block/budget block/review block의 state/locator/status 연결을 한 번에 보장하지 않는다.
- 해결 방법: `DynamicSelectorAuditTest`에서 고정 시각과 quota snapshot으로 대표 work unit을 실행하고 `work_unit_id`, `rule_id`, `policy_priority`, candidate rank/selected target, reason codes, quota snapshot/status, transition과 budget을 persisted state, invocation, locator, status/dry-run에서 대조한다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: `DynamicSelectorAuditTest`.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: 필요한 reason/transition matrix 보강.
- 테스트 작성: `test_dispatch.py``DynamicSelectorAuditTest`에 success/pinned resume, 양방향 initial의 qualified provider target 불가 failover와 generic same-target recovery, unknown 1회→confirmed exhausted quota blocker, 10회 budget blocker, user-review blocked branch와 독립 drain, completing-target selfcheck, batch snapshot case를 표 fixture로 연결한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicSelectorAuditTest -v` — S12 다섯 outcome과 evidence-field assertions가 PASS해야 한다.
### [TEST-2] 운영 skill 정책 동기화
- 문제: `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md:36-52,159-190`의 정적 route matrix, 승격/자가검증/동시성 설명이 runtime selector의 시간·quota·persisted transition 정책과 다르다.
- 해결 방법:
```text
# Before: agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md:36
PLAN lane/G → 고정 model 및 target별 동시 실행 제한
# After
PLAN lane/G → runtime selector input → persisted adapter/target,
admission batch snapshot 재사용, target별 정책 cap 없음
```
worker와 공식 review를 분리하고 KST 경계, cloud grade matrix, route pin, failover logical context, stage budget 10회, 실제 완료 target selfcheck를 정확히 기술한다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md`: route/실행/복구/검증 설명.
- 테스트 작성: 없음. 문서 변경이므로 TEST-3의 deterministic search와 구현 suite로 검증한다.
- 중간 검증: `bash -c 'matches=$(rg --sort path -n '"'"'Gemini 3\\.5|동시 실행 제한|route_agent\\(|invocation_semaphore|^[[:space:]]*semaphores = \\{'"'"' agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py); rc=$?; test "$rc" -eq 1 && test -z "$matches"'` — 금지 match 0건으로 exit 0이어야 한다.
### [TEST-3] Drift guard와 전체 회귀
- 문제: `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md:36-52``dispatch.py:977,2696,3755-3761`의 구 target/static route/cap이 재도입돼도 현재 suite가 정책 drift로 명시적으로 실패하지 않을 수 있다.
- 해결 방법: source-level assertion은 완전 제거하기로 한 production symbol과 운영 문구에만 제한하고 behavior test를 우선한다. `route_agent` compatibility helper도 `05`에서 제거되므로 허용 production match는 두지 않는다.
- 수정 파일 및 체크리스트:
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: behavior 중심 drift guard.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`: canonical matrix 누락 시 보강.
- [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`: JSON evidence 필드 누락 시 보강.
- 테스트 작성: `test_execution_target_policy.py`, `test_select_execution_target.py`, `test_dispatch.py`에서 G01~G10 worker/review matrix와 SDD S05~S12 대표 흐름을 전체 suite로 실행한다.
- 중간 검증: `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — fresh process 전체 suite가 PASS해야 한다.
## 의존 관계 및 구현 순서
1. `06+05_selfcheck_policy``07+06_throughput_policy`의 archived `complete.log`가 모두 PASS여야 한다. 계획 작성 시점에는 둘 다 미충족이다.
2. TEST-1로 실제 evidence shape를 고정하고 TEST-2 문서를 그 결과에 맞춘 뒤 TEST-3으로 drift를 닫는다.
3. 이 task PASS 후 `sync-milestone-workstate``audit-tests`를 반영하면 마일스톤 완료 검토가 가능하다.
## 수정 파일 요약
| 파일 | 항목 |
|---|---|
| `agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md` | TEST-2 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py` | TEST-1, TEST-3 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py` | TEST-3 |
| `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py` | TEST-1, TEST-3 |
## 최종 검증
- `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py DynamicSelectorAuditTest -v` — S12 audit matrix 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/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.
- `bash -c 'matches=$(rg --sort path -n '"'"'Gemini 3\\.5|동시 실행 제한|route_agent\\(|invocation_semaphore|^[[:space:]]*semaphores = \\{'"'"' agent-ops/skills/project/orchestrate-agent-task-loop/SKILL.md agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py); rc=$?; test "$rc" -eq 1 && test -z "$matches"'` — 금지 production match 0건, exit 0.
- `git diff --check` — 출력 없음.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.