feat(agent-ops): 로컬 우선 라우팅 경계를 보강한다

로컬 모델의 작업 비중을 유지하면서 복합 위험과 반복 실패가 누적될 때만 cloud로 승격하고, 라우팅 자체의 재분석 비용과 반복 루프를 줄이기 위함이다.
This commit is contained in:
toki 2026-07-27 19:57:23 +09:00
parent 1550e8cacc
commit 0e12a540c8
8 changed files with 351 additions and 382 deletions

View file

@ -189,6 +189,7 @@ Required fields:
- `종합 판정`: exactly `PASS`, `WARN`, or `FAIL`.
- `차원별 평가`: Pass/Warn/Fail for correctness, completeness, test coverage, API contract, code quality, implementation deviation, verification trust. If SDD Evidence Map applies through `Roadmap Targets`, also include spec conformance.
- `발견된 문제`: `없음`, or bullets using `Required`, `Suggested`, or `Nit` with `file:line` and a concrete fix.
- `라우팅 신호`: calculate once and append `review_rework_count=<N>` and `evidence_integrity_failure=true|false`. Set rework count to archived same-task `WARN|FAIL` verdicts plus one only when the current verdict is non-PASS. Set integrity failure to true only when a claimed test, command, exit code, or production path is absent, unexecuted, or contradicted by fresh reviewer evidence.
- `다음 단계`: keep only the matching PASS, WARN/FAIL follow-up, or USER_REVIEW line.
Do not check archive/next-state items in `코드리뷰 전용 체크리스트` during Step 4. Complete the applicable dedicated checklist items in the archived `code_review_*.log` during Step 7, after archive, next-state writes, and PASS task-artifact moves are done.
@ -222,13 +223,13 @@ Do not archive WARN/FAIL files until the next-state content is fully prepared in
- If the gate triggers, render the complete `USER_REVIEW.md` body from `agent-ops/skills/common/code-review/templates/user-review-template.md` in memory.
- Otherwise build the follow-up handoff described below and fully execute `agent-ops/skills/common/plan/SKILL.md` in `prepare-follow-up` mode.
Before preparing a follow-up, count the existing logs and parse each current active basename. Set `current_review_archive_number=count(code_review_*.log)` and `current_plan_archive_number=count(plan_*.log)`. Derive `current_review_archive_name` and `current_plan_archive_name` from the current active files' own lane/grade plus those numbers. These names describe the pair being closed, not the next route.
Reuse the routing signals appended in Step 4; do not recount verdict history for routing. Separately count the existing logs once for archive identity: set `current_review_archive_number=count(code_review_*.log)` and `current_plan_archive_number=count(plan_*.log)`, then derive both archive names from the current active files' own lane/grade. These archive values describe the pair being closed, not the next route.
The follow-up handoff contains the selected `{task_name}`, current verdict, Required/Suggested/Nit findings, affected files, actual verification output, current ownership/dependency facts, roadmap carryover, `REVIEW_<PARENT_TAG>`, and those predicted current-pair archive names. Keep current active paths only as evidence pointers. Do not add the prior lane, grade, routing score, rationale, or a preferred next route to the neutral routing snapshot, and require plan to omit route-bearing basenames from the isolated routing input. The plan may use current archive names only after routing to render `Archive Evidence Snapshot`.
The follow-up handoff contains the selected `{task_name}`, the current plan's requested outcome/acceptance/exclusions revalidated against current evidence, current verdict, Required/Suggested/Nit findings, affected files, actual verification output, current ownership/dependency facts, roadmap carryover, `review_rework_count`, `evidence_integrity_failure`, `REVIEW_<PARENT_TAG>`, and those predicted current-pair archive names. Keep current active paths only as evidence pointers. Do not add the prior lane, grade, routing score, rationale, or a preferred next route to the neutral routing snapshot, and require plan to omit route-bearing basenames from the isolated routing input. The plan may use current archive names only after routing to render `Archive Evidence Snapshot`.
- `prepare-follow-up` must return `status: routed`, the exact routed basenames, `prepared_plan`, `prepared_review`, `plan_number`, `current_plan_archive_name`, `current_plan_archive_number`, `current_review_archive_name`, `current_review_archive_number`, `plan_log_number`, `review_log_number`, and `gitignore_repair_needed`. It must have executed `finalize-task-routing` in `isolated-reassessment` mode.
- Verify that the returned current archive names/numbers equal the values derived before preparation, and that `plan_log_number` / `review_log_number` are the post-archive counts embedded in the new review stub for its future archive.
- If preparation returns `needs_evidence`, continue evidence collection and rerun it before archiving.
- If preparation returns `needs_evidence`, collect all named new evidence and rerun after the input changes; never rerun with unchanged evidence. If the evidence cannot be obtained in the current scope, leave the verdict-appended pair in place and report the exact finalization blocker.
- If preparation returns `blocked`, leave the verdict-appended active PLAN/CODE_REVIEW pair in place, do not check archive/next-state items, and report a resumable finalization blocker. A later code-review invocation resumes this step without appending another verdict.
After the required next state is prepared, archive is mandatory for `PASS`, `WARN`, and `FAIL`. Ensure `.gitignore` has the Agent-Ops managed gitignore block for task artifacts before writing `*.log` outputs. Prefer `source agent-ops/bin/ai-ignore.sh && agent_ops_ensure_gitignore_task_artifact_block .gitignore`; if the helper is unavailable, add or update a block containing `!agent-task/`, `!agent-task/**/`, `!agent-task/**/*.md`, `!agent-task/**/*.log`, and `agent-roadmap/current.md`. Apply the repair here when `prepare-follow-up` returned `gitignore_repair_needed: true`.
@ -262,7 +263,7 @@ For `WARN` or `FAIL`, materialize the next state prepared in Step 5 immediately
- If the user-review gate triggered, write the prepared body to `agent-task/{task_name}/USER_REVIEW.md`. It must use only `milestone-lock`, contain every archived loop entry plus the exact linked decision, and contain no placeholder. Do not write active PLAN/CODE_REVIEW files or `complete.log`.
- Otherwise write `prepared_plan` and `prepared_review` byte-for-byte to their routed basenames. Do not rerun, adjust, compare, or upgrade their lane/G after archive.
- Verify the written follow-up pair contains the predicted archived plan/review paths in identical `Archive Evidence Snapshot` sections and contains no unresolved token from the review-stub template inventory. Unrelated braces in commands or code are allowed.
- A prior local failure never changes the prepared pair into an automatic cloud upgrade.
- Do not adjust the prepared route after finalization. For a `local-fit` base, `review_rework_count >= 2` or `evidence_integrity_failure=true` must produce `recovery-boundary`; `capability-gap` and `grade-boundary` keep their own basis.
If the task group is `m-<milestone-slug>` and the user-review gate triggered, report that the milestone task is blocked on user review; do not emit PASS completion metadata and do not call `update-roadmap`.
@ -325,8 +326,8 @@ Report Required/Suggested counts, archive names, the final task archive path for
- PASS with `Agent UI Completion`: listed agent-ui docs were updated to `구현됨` with code evidence, `validate-agent-ui` passed, and `complete.log` contains `Agent UI Completion`.
- PASS without `Agent UI Completion`: complete.log omits `Agent UI Completion`.
- PASS single/split: task-group `WORK_LOG.md` was not moved, copied, deleted, or renamed by review. Split moved the selected subtask directory and left its group parent for the dispatcher; single left only `WORK_LOG.md` in its task directory. A parent without `WORK_LOG.md` is removed only when empty.
- WARN/FAIL without user-review gate: the plan skill was invoked for the exact task path, completed `finalize-task-routing`, and created new active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md` files matching the fresh routed output; no `complete.log`.
- WARN/FAIL follow-up: the plan input omitted prior lane/G assessment, and the new active plan/review stub contain identical `Archive Evidence Snapshot` sections with prior archived plan/review log paths, verdict, issue summary, affected files, verification evidence, and specific archive paths allowed for narrow reread when needed.
- WARN/FAIL without user-review gate: the plan skill was invoked for the exact task path with verified `review_rework_count` and `evidence_integrity_failure`, completed `finalize-task-routing`, and created new active `PLAN-{build_lane}-GNN.md` and `CODE_REVIEW-{review_lane}-GNN.md` files matching the fresh routed output; no `complete.log`.
- WARN/FAIL follow-up: the plan input omitted prior route fields, revalidated outcome/acceptance/exclusions from current evidence, used the completed in-memory PLAN as the packet, and copied identical `Archive Evidence Snapshot` sections into the new plan/review pair.
- Follow-up plans and review stubs keep implementation agents limited to implementation/test/evidence plus task-local work-log fields and contain no implementation-owned user-review request section.
- USER_REVIEW: `USER_REVIEW.md` exists from template, no active `PLAN-*.md` or `CODE_REVIEW-*.md` remains, and no `complete.log` was written.
- Review-agent-owned USER_REVIEW: the generated `USER_REVIEW.md` records the exact active Milestone decision and repository evidence that made automatic continuation unsafe.

View file

@ -1,90 +1,85 @@
---
name: finalize-task-routing
description: PLAN/CODE_REVIEW 작성 직전 build는 local-first로 라우팅하고 review는 실제 G와 고정 공식 리뷰 target을 확정한다. capability-gap 기반 cloud 선택과 follow-up 재평가에 사용한다.
description: PLAN/CODE_REVIEW 작성 직전 완성된 build packet을 한 번 평가해 local-first lane, G, canonical filename을 확정한다. 최초 plan과 WARN/FAIL follow-up에서 capability gap, indivisible context, 복합 loop-risk, review rework, evidence integrity에 따른 cloud 경계를 적용할 때 사용한다.
---
# Finalize Task Routing
## 목
## 목
build의 lane/G와 review의 G를 평가해 canonical 파일명을 확정한다.
공식 review는 항상 cloud의 Codex `gpt-5.6-sol` xhigh다. 이 스킬은 task 파일을 수정하지 않는다.
## 호출 시점
- `plan`이 최초 또는 follow-up pair를 쓰기 직전
- scope, 증거, 소유권, 결정 조건이 달라져 기존 route가 무효가 된 때
- 사용자가 현재 pair의 lane/G 판정을 요청한 때
완성된 in-memory PLAN 하나를 한 번 평가해 build/review route를 확정한다. routing 전용 문서나 증거 탐색을 만들지 않는다. Build의 기본값은 local이며 아래 표의 cloud 조건에 일치할 때만 승격한다. 공식 review는 항상 cloud의 Codex `gpt-5.6-sol` xhigh다. 이 스킬은 task 파일을 수정하지 않는다.
## 입력
- `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, 변경된 범위 등 재평가 근거
- `targets.build`: 구현 에이전트가 읽을 PLAN과 PLAN 작성에 이미 사용한 scope, source/test/contract, 불변조건, 검증 근거
- `targets.review`: review scope, 영향 경로, 검증 계획과 근거
- target별 closure와 근거: `scope_closed`, `context_closed`, `verification_closed`, `evidence_trusted`, `ownership_closed`, `decision_closed`
- target별 G 점수: `scope_coupling`, `state_concurrency`, `blast_irreversibility`, `evidence_diagnosis`, `verification_complexity`
- build 신호: `large_indivisible_context`, `matched_loop_risk_signatures`, `review_rework_count`, `evidence_integrity_failure`
- closure가 false이고 cloud가 해소할 수 있을 때만 `capability_gap`: `observed`, `evidence`, `cloud_resolution`
이전 lane/G, 점수, 라우팅 사유와 filename은 입력에서 제외한다. 이전 로그와 finding은 원본에서 확인한 사실만 사용한다. 확인된 부재는 0/false/빈 목록, 미확인은 `unknown`다.
이전 lane/G, 점수, route 사유, filename을 입력하지 않는다. `review_rework_count``evidence_integrity_failure`만 route-free 운영 이력으로 전달한다.
## 절차
## 판정 순서
### 1. 평가 격리
아래 순서를 지키고 종료 조건을 만나면 즉시 멈춘다.
각 target을 `unrouted`로 시작한다. `isolated-reassessment`는 route 정보가 제거된 현재 snapshot만 평가한다. 중립 snapshot을 만들 수 없으면 `blocked: isolated-routing-unavailable`을 반환한다.
### 1. Snapshot 고정
실패·review 횟수, Required/finding 개수와 이전 grade는 승격 조건이나 점수가 아니다.
PLAN body, split, verification, evidence, ownership, decision이 완성된 뒤 실행한다.
### 2. 증거 gate
- PLAN 자체를 build packet으로 사용한다. 별도 packet 요약을 만들지 않는다.
- 이미 읽은 사실만 사용한다. routing 때문에 source/test/log를 다시 읽지 않는다.
- split된 packet만 평가한다. parent/sibling의 파일·상태·risk를 합산하지 않는다.
- `isolated-reassessment`에서는 이전 route 정보를 제거한다. 제거할 수 없으면 `blocked: isolated-routing-unavailable`로 종료한다.
build와 review에 다음 값을 독립적으로 `true | false | unknown`으로 판정하고 파일·diff·명령 출력 근거를 한 줄씩 기록한다.
### 2. Closure gate
- `scope_closed`: 정확성 조건과 영향 경로가 확정됐는가
- `context_closed`: 중요한 증거를 빠뜨리지 않고 함께 다룰 수 있는가
- `verification_closed`: 결정적 명령과 관찰로 성공을 판정할 수 있는가
- `evidence_trusted`: 검증이 실제 경로를 실행하며 재현 가능한가
- `ownership_closed`: 공유 상태와 외부 소유권이 명확한가
- `decision_closed`: 사용자 결정이나 외부 의미 판단 없이 완료할 수 있는가
각 target의 closure를 독립적으로 판정한다.
파일 수, 긴 문서, 동시성, dirty worktree와 finding 수만으로 `false`를 주지 않는다.
- `scope_closed`: 정확성 조건과 영향 경로가 확정됨
- `context_closed`: PLAN과 pointer가 구현에 필요한 사실을 제공함
- `verification_closed`: 결정적 성공 판정 방법이 있음
- `evidence_trusted`: 실제 경로를 실행하는 재현 가능한 근거가 있음
- `ownership_closed`: 공유 상태와 외부 소유권이 명확함
- `decision_closed`: 사용자·외부 의미 판단 없이 완료 가능함
- closure 또는 build loop-risk 필드가 하나라도 `unknown`이면 `needs_evidence`다. 이를 `false`로 낮추지 않는다.
- closure `false`가 권한, secret, 사용자 결정, 환경·의존성 또는 신뢰할 증거 부재라면 `blocked`다.
- closure `false`를 cloud capability가 해소할 수 있을 때만 `capability-gap` 후보로 둔다.
첫 번째로 일치하는 규칙을 적용한다.
### 3. G 등급
| 조건 | 결과 |
|------|------|
| 어느 target이든 closure 하나라도 `unknown` | `needs_evidence`; 부족한 evidence를 구체적으로 기록하고 종료 |
| 어느 target이든 closure 하나라도 `false`이고 구체적인 `cloud_resolution`이 없음 | `blocked`; 해제 조건을 기록하고 종료 |
| build closure가 `false`이고 `observed`, `evidence`, `cloud_resolution`이 모두 있음 | build base basis를 `capability-gap`으로 설정 |
| review closure만 `false`이고 cloud가 해소함 | review gap만 기록; build basis에는 영향 없음 |
| build closure가 모두 `true` | G를 계산해 build base basis 결정 |
각 target을 다음 다섯 차원에서 독립적으로 `0`~`2`로 채점한다.
`needs_evidence`는 확인된 모든 missing evidence를 한 번에 나열한다. 입력이 달라진 뒤에만 재평가하고 같은 evidence로 반복하지 않는다.
### 3. G 계산
각 target의 다섯 값을 `0`~`2`로 채점하고 `grade=max(1, 합계)`로 계산한다.
- `scope_coupling`: 국소 / 여러 연결 지점 / 다중 모듈·계약
- `state_concurrency`: 무상태 / 상태 전이 / 공유 상태·순서·동시성
- `blast_irreversibility`: 내부 가역 / 다중 호출자·지속 상태 / 외부 계약·권한·비가역
- `blast_irreversibility`: 내부 가역 / 다중 호출자·지속 상태 / 외부 계약·비가역
- `evidence_diagnosis`: 직접 증거 / 복수 증거 조합 / 충돌·불신 증거 진단
- `verification_complexity`: 단일 명령 / 복수 단계·통합 / 외부·상호작용·장시간
`grade = max(1, 점수 합)`이다. lane과 반복 횟수는 점수에 더하지 않는다.
capability gap이 없으면 G01~G08은 `local-fit`, G09~G10은 `grade-boundary`다.
### 4. Route basis
### 4. Build 신호 계산
build basis를 하나만 선택한다.
`large_indivisible_context=true`는 다음 세 조건을 모두 만족할 때만 사용한다.
- `local-fit`: 모든 closure가 `true`이고 G01~G08
- `grade-boundary`: 모든 closure가 `true`이고 G09~G10
- `capability-gap`: 하나 이상의 closure `false`를 cloud가 실제로 해소함
1. 안전한 split을 끝냈다.
2. 하나의 correctness/transaction 판단이 여러 독립 source of truth를 동시에 요구한다.
3. 명시적 국소 규칙과 결정적 verification oracle로 축약할 수 없다.
`capability-gap`에는 관측된 gap, source/test/diff/log 증거, cloud가 해소하는 방법을 모두 기록한다. 이 세 근거 없는 G01~G08 build cloud route는 금지한다. G09~G10에 실제 gap도 있으면 basis는 `capability-gap`으로 두고 grade 점수도 함께 보존한다.
파일이 많다는 이유만으로 true로 두지 않는다.
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를 현재 증거로 기록한다.
아래 signature를 이미 읽은 PLAN/source/test/contract에서 한 번만 확인한다. 양성 조건이 보인 이름만 기록하고, match하지 않은 signature의 부재를 증명하지 않는다. 필요한 범위가 불명확하면 risk를 `unknown`으로 만들지 말고 closure에서 처리한다.
- `temporal_state`: 상태 3개 이상과 partial/retry/cancel/recovery/stale/duplicate 경로
- `concurrent_consistency`: actor 2개 이상과 atomic snapshot/lock order/exactly-once/race-free 제약
@ -92,71 +87,63 @@ build의 다음 signature를 현재 증거로 기록한다.
- `structured_interpretation`: parser/lexer/protocol assembly/multi-source merge와 nesting/quoting/precedence/deduplication 위험
- `variant_product`: 독립 variant 축 2개 이상과 조합 검증
signature와 `triggered`는 audit 정보다. lane/G를 바꾸지 않는다.
중복 없는 이름 수를 `loop_risk_count=0..5`로 사용한다.
WARN/FAIL 후에는 현재 scope를 다시 평가한다. local에서 닫히면 local을 유지하고, plan/split/fixture/test 문제면 local에서 재구성한다. 실제 capability gap만 해당 새 PLAN 세대를 cloud로 보낸다. 고정 시도 횟수나 finding 재발로 cloud를 확정하지 않는다.
- `risk_boundary = large_indivisible_context || loop_risk_count >= 3`
- `recovery_boundary = review_rework_count >= 2 || evidence_integrity_failure`
### 6. Canonical filename
`review_rework_count`는 같은 task path의 archived `WARN|FAIL` 수에 현재 non-PASS verdict를 더한 값이다. 첫 non-PASS follow-up은 1, 두 번째는 2다.
모든 gate를 통과한 뒤 `pair` mode를 정확히 한 번 실행한다. finalizer는 lane을 입력받지 않고 route basis와 grade에서 lane을 산출한다.
### 5. Build route 확정
첫 번째로 일치하는 규칙을 적용한다.
| 조건 | `route_basis` | lane |
|------|---------------|------|
| base basis가 `capability-gap` | `capability-gap` | cloud |
| base basis가 `grade-boundary` | `grade-boundary` | cloud |
| base basis가 `local-fit`이고 `recovery_boundary` | `recovery-boundary` | cloud |
| base basis가 `local-fit`이고 `risk_boundary` | `risk-boundary` | cloud |
| 그 외 | `local-fit` | local |
risk/recovery는 local-fit build의 승격 조건이다. 이미 cloud인 capability/grade basis를 덮어쓰지 않는다. grade는 lane 변경과 무관하게 보존한다. review는 `official-review`, cloud, 실제 G를 사용한다.
### 6. Finalizer 실행
`status=routed`일 때만 아래 명령을 정확히 한 번 실행한다.
```bash
agent-ops/skills/common/finalize-task-routing/scripts/finalize-task-policy.sh pair \
<build-route-basis> <true|false> \
<build-base-basis> \
<large-indivisible-context> <loop-risk-count> \
<review-rework-count> <evidence-integrity-failure> \
<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을 수작업으로 만들지 않는다.
finalizer 출력만 사용한다. lane, grade, boundary, 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이 일치하는가
항상 `status`, `evaluation_mode`, `missing_evidence`, `blocked_reason`을 반환한다. `status=routed`이면 다음 필드를 모두 반환한다.
## 출력 형식
- 공통: `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`
- target별: `closures`, `closure_basis`, `capability_gap`, `grade_scores`, `route_basis`, `lane`, `grade`, `filename`
- build 전용: `base_route_basis`, `large_indivisible_context`, `matched_loop_risk_signatures`, `loop_risk_count`, `review_rework_count`, `evidence_integrity_failure`, `risk_boundary_matched`, `recovery_boundary_matched`
- review 전용: `route_basis=official-review`, `adapter=codex`, `model=gpt-5.6-sol`, `reasoning_effort=xhigh`
```yaml
status: routed | needs_evidence | blocked
evaluation_mode: first-pass | isolated-reassessment
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: "<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: 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
```
## 완료 확인
`capability-gap`이면 `capability_gap``observed`, `evidence`, `cloud_resolution`을 기록한다. `needs_evidence`/`blocked`에서는 finalizer와 route 결과를 `null`로 둔다.
- 기존 PLAN 분석만 사용했고 routing-only 탐색을 하지 않았는가
- split packet마다 독립 평가했는가
- G01~G08 cloud route가 선택된 `route_basis` 하나와 일치하는 근거를 갖는가
- G09~G10은 risk/recovery 신호가 있어도 `grade-boundary`를 유지하는가
- finalizer 출력과 active filename이 일치하는가
## 금지 사항
## 금지
- 이전 route나 반복 횟수로 자동 승격하지 않는다.
- signature로 cloud lane 또는 G floor를 강제하지 않는다.
- unmatched signature의 부재를 찾기 위해 추가 탐색하지 않는다.
- 이전 lane/G나 finding 개수를 승격 근거로 쓰지 않는다.
- cloud가 해소할 수 없는 문제를 cloud route로 바꾸지 않는다.
- review에 universal cloud/G floor를 적용하지 않는다.
- PLAN, CODE_REVIEW, archive와 complete.log를 직접 수정하지 않는다.
- PLAN, CODE_REVIEW, archive, complete.log를 직접 수정하지 않는다.

View file

@ -1,4 +1,4 @@
interface:
display_name: "Finalize Task Routing"
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."
short_description: "Route plans once with explicit cloud boundaries"
default_prompt: "Use $finalize-task-routing once on the completed plan to select local or cloud from explicit capability, grade, risk, and recovery boundaries."

View file

@ -4,8 +4,9 @@ set -euo pipefail
usage() {
printf '%s\n' \
'usage:' \
' 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
' finalize-task-policy.sh pair <build-base-basis> <large-indivisible-context> <loop-risk-count> <review-rework-count> <evidence-integrity-failure> <build-scope> <build-state> <build-blast> <build-evidence> <build-verification> official-review <review-scope> <review-state> <review-blast> <review-evidence> <review-verification>' \
' build-base-basis: local-fit | capability-gap | grade-boundary' \
' boolean fields: true | false; loop-risk-count: 0..5; review-rework-count: non-negative integer' >&2
exit 2
}
@ -25,40 +26,64 @@ field() {
}
finalize_build() {
local route_basis=$1
local base_basis=$1
shift
local scores=("$@")
local lane
local route
case "$route_basis" in
local-fit)
lane=local
;;
capability-gap|grade-boundary)
lane=cloud
case "$base_basis" in
local-fit|capability-gap|grade-boundary)
;;
*)
usage
;;
esac
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)
BUILD_BASE_ROUTE_BASIS=$base_basis
BUILD_RISK_BOUNDARY_MATCHED=false
BUILD_RECOVERY_BOUNDARY_MATCHED=false
if [[ "$BUILD_LARGE_INDIVISIBLE_CONTEXT" == true ||
"$BUILD_LOOP_RISK_COUNT" -ge 3 ]]; then
BUILD_RISK_BOUNDARY_MATCHED=true
fi
if [[ "$BUILD_EVIDENCE_INTEGRITY_FAILURE" == true ||
"$BUILD_REVIEW_REWORK_COUNT" -ge 2 ]]; then
BUILD_RECOVERY_BOUNDARY_MATCHED=true
fi
if [[ "$route_basis" == local-fit &&
if [[ "$base_basis" == capability-gap ]]; then
BUILD_ROUTE_BASIS=capability-gap
elif [[ "$base_basis" == local-fit &&
"$BUILD_RECOVERY_BOUNDARY_MATCHED" == true ]]; then
BUILD_ROUTE_BASIS=recovery-boundary
elif [[ "$base_basis" == local-fit &&
"$BUILD_RISK_BOUNDARY_MATCHED" == true ]]; then
BUILD_ROUTE_BASIS=risk-boundary
else
BUILD_ROUTE_BASIS=$base_basis
fi
if [[ "$BUILD_ROUTE_BASIS" == local-fit ]]; then
lane=local
else
lane=cloud
fi
route=$("$formatter" build "$lane" "${scores[@]}")
BUILD_GRADE=$(field "$route" grade)
if [[ "$base_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 &&
if [[ "$base_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 "$route" lane)
BUILD_FILENAME=$(field "$route" filename)
}
finalize_review() {
@ -79,8 +104,14 @@ finalize_review() {
}
emit_build() {
printf 'build_base_route_basis=%s\n' "$BUILD_BASE_ROUTE_BASIS"
printf 'build_route_basis=%s\n' "$BUILD_ROUTE_BASIS"
printf 'build_risk_triggered=%s\n' "$BUILD_RISK_TRIGGERED"
printf 'build_large_indivisible_context=%s\n' "$BUILD_LARGE_INDIVISIBLE_CONTEXT"
printf 'build_loop_risk_count=%s\n' "$BUILD_LOOP_RISK_COUNT"
printf 'build_risk_boundary_matched=%s\n' "$BUILD_RISK_BOUNDARY_MATCHED"
printf 'build_review_rework_count=%s\n' "$BUILD_REVIEW_REWORK_COUNT"
printf 'build_evidence_integrity_failure=%s\n' "$BUILD_EVIDENCE_INTEGRITY_FAILURE"
printf 'build_recovery_boundary_matched=%s\n' "$BUILD_RECOVERY_BOUNDARY_MATCHED"
printf 'build_lane=%s\n' "$BUILD_LANE"
printf 'build_grade=%s\n' "$BUILD_GRADE"
printf 'build_filename=%s\n' "$BUILD_FILENAME"
@ -97,17 +128,39 @@ emit_review() {
}
mode=${1:-}
[[ "$mode" == pair && $# -eq 14 ]] || usage
[[ "$mode" == pair && $# -eq 17 ]] || usage
case "$3" in
true|false)
BUILD_RISK_TRIGGERED=$3
BUILD_LARGE_INDIVISIBLE_CONTEXT=$3
;;
*)
usage
;;
esac
finalize_build "$2" "${@:4:5}"
finalize_review "$9" "${@:10:5}"
case "$4" in
0|1|2|3|4|5)
BUILD_LOOP_RISK_COUNT=$4
;;
*)
usage
;;
esac
[[ "$5" =~ ^[0-9]+$ ]] || usage
BUILD_REVIEW_REWORK_COUNT=$5
case "$6" in
true|false)
BUILD_EVIDENCE_INTEGRITY_FAILURE=$6
;;
*)
usage
;;
esac
finalize_build "$2" "${@:7:5}"
finalize_review "${12}" "${@:13:5}"
printf 'finalizer=finalize-task-policy.sh\n'
printf 'finalizer_mode=%s\n' "$mode"

View file

@ -10,6 +10,7 @@ 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"
PLAN_SKILL = SKILL_DIR.parent / "plan" / "SKILL.md"
GRADE_VECTORS = {
1: (0, 0, 0, 0, 0),
@ -53,13 +54,19 @@ def pair_args(
build_basis: str,
build_grade: int,
review_grade: int,
risk: str = "false",
large_context: str = "false",
risk_count: int = 0,
review_rework_count: int = 0,
evidence_integrity: str = "false",
review_basis: str = "official-review",
) -> tuple[object, ...]:
return (
"pair",
build_basis,
risk,
large_context,
risk_count,
review_rework_count,
evidence_integrity,
*GRADE_VECTORS[build_grade],
review_basis,
*GRADE_VECTORS[review_grade],
@ -83,6 +90,32 @@ class FinalizeTaskRoutingTests(unittest.TestCase):
result[f"{target}_filename"], f"{prefix}-{lane}-G{grade:02d}.md"
)
def test_request_to_worker_contract_is_ordered_and_consistent(self) -> None:
routing_skill = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
plan_skill = PLAN_SKILL.read_text(encoding="utf-8")
self.assertIn("`matched_loop_risk_signatures`", routing_skill)
self.assertIn("`temporal_state`", routing_skill)
self.assertNotIn("`ordered_transitions`", routing_skill)
self.assertIn("routing 때문에 source/test/log를 다시 읽지", routing_skill)
self.assertIn("첫 번째로 일치하는 규칙", routing_skill)
self.assertIn("이미 cloud인 capability/grade basis를 덮어쓰지 않는다", routing_skill)
self.assertIn("review gap만 기록; build basis에는 영향 없음", routing_skill)
markers = (
"**Assess split boundaries once**",
"**Derive routing signals once**",
"## Step 3 - Finalize Task Routing",
)
positions = [plan_skill.index(marker) for marker in markers]
self.assertEqual(positions, sorted(positions))
self.assertNotIn("**Extract the request contract**", plan_skill)
self.assertNotIn("`요청 계약`", plan_skill)
self.assertIn("reuse the values already validated and appended", plan_skill)
policy_script = POLICY_FINALIZER.read_text(encoding="utf-8")
self.assertNotIn("build_risk_triggered", policy_script)
self.assertNotIn("boundary_triggered", policy_script)
def test_formatter_grade_and_filename_matrix(self) -> None:
for target, lane, grade in itertools.product(
("build", "review"), ("local", "cloud"), range(1, 11)
@ -159,28 +192,126 @@ class FinalizeTaskRoutingTests(unittest.TestCase):
)
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_loop_risk_boundary_routes_three_or_more_signatures_to_cloud(self) -> None:
for risk_count in range(6):
with self.subTest(risk_count=risk_count):
result = fields(
run(
POLICY_FINALIZER,
*pair_args("local-fit", 7, 6, risk_count=risk_count),
).stdout
)
self.assertEqual(result["build_loop_risk_count"], str(risk_count))
expected_basis = "risk-boundary" if risk_count >= 3 else "local-fit"
expected_lane = "cloud" if risk_count >= 3 else "local"
self.assertEqual(result["build_base_route_basis"], "local-fit")
self.assert_route(result, "build", expected_basis, expected_lane, 7)
def test_large_indivisible_context_routes_to_cloud(self) -> None:
result = fields(
run(
POLICY_FINALIZER,
*pair_args("local-fit", 4, 4, large_context="true"),
).stdout
)
self.assertEqual(result["build_risk_boundary_matched"], "true")
self.assert_route(result, "build", "risk-boundary", "cloud", 4)
def test_review_rework_and_evidence_integrity_route_to_cloud(self) -> None:
one_rework = fields(
run(
POLICY_FINALIZER,
*pair_args("local-fit", 4, 4, review_rework_count=1),
).stdout
)
self.assert_route(one_rework, "build", "local-fit", "local", 4)
two_reworks = fields(
run(
POLICY_FINALIZER,
*pair_args("local-fit", 4, 4, review_rework_count=2),
).stdout
)
self.assert_route(
two_reworks, "build", "recovery-boundary", "cloud", 4
)
evidence_failure = fields(
run(
POLICY_FINALIZER,
*pair_args("local-fit", 4, 4, evidence_integrity="true"),
).stdout
)
self.assert_route(
evidence_failure, "build", "recovery-boundary", "cloud", 4
)
recovery_over_risk = fields(
run(
POLICY_FINALIZER,
*pair_args(
"local-fit",
4,
4,
large_context="true",
review_rework_count=2,
),
).stdout
)
self.assertEqual(recovery_over_risk["build_base_route_basis"], "local-fit")
self.assert_route(
recovery_over_risk, "build", "recovery-boundary", "cloud", 4
)
def test_grade_boundary_is_not_relabelled_by_escalation_signals(self) -> None:
result = fields(
run(
POLICY_FINALIZER,
*pair_args(
"grade-boundary",
9,
4,
large_context="true",
risk_count=5,
review_rework_count=2,
evidence_integrity="true",
),
).stdout
)
self.assertEqual(result["build_base_route_basis"], "grade-boundary")
self.assertEqual(result["build_risk_boundary_matched"], "true")
self.assertEqual(result["build_recovery_boundary_matched"], "true")
self.assert_route(result, "build", "grade-boundary", "cloud", 9)
def test_capability_gap_has_precedence_over_other_cloud_triggers(self) -> None:
result = fields(
run(
POLICY_FINALIZER,
*pair_args(
"capability-gap",
4,
4,
large_context="true",
risk_count=5,
review_rework_count=2,
evidence_integrity="true",
),
).stdout
)
self.assertEqual(result["build_base_route_basis"], "capability-gap")
self.assert_route(result, "build", "capability-gap", "cloud", 4)
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]),
("pair", "local-fit", "unknown", 0, 0, "false", *valid, "official-review", *valid),
("pair", "local-fit", "false", 6, 0, "false", *valid, "official-review", *valid),
("pair", "local-fit", "false", 0, -1, "false", *valid, "official-review", *valid),
("pair", "local-fit", "false", 0, 0, "unknown", *valid, "official-review", *valid),
("pair", "local", "false", 0, 0, "false", *valid, "official-review", *valid),
("pair", "local-fit", "false", 0, 0, "false", *valid, "cloud", *valid),
("build", "local-fit", "false", 0, 0, "false", *valid),
("pair", "local-fit", "false", 0, 0, "false", *valid, "official-review", *valid[:-1]),
)
for args in invalid_cases:
with self.subTest(args=args):

View file

@ -1,194 +0,0 @@
#!/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

@ -59,23 +59,18 @@ Role boundary rules:
Split decision policy:
- Default to multiple subtask directories under one task group. Before writing any plan, decide whether the work has smaller independently reviewable implementation units.
- If there is any natural dependency boundary, ownership boundary, subsystem boundary, API-vs-call-site phase, test strategy split, or independently reviewable risk, write multiple subtask directories under one task group.
- Split by independently verifiable behavior or contract slices, not by production/test/fixture artifact type.
- Keep each production change with the tests required by the Test policy for its PASS. A test-only child is allowed only for additional integration or closure evidence across completed production children.
- A shared package, helper, or validation pattern is not a merge reason when two public contract families can be implemented and tested independently.
- A single plan is an exception. Use one only when all of these are true: the work is one coherent implementation unit, it has one primary ownership boundary, it has no prerequisite subtask, splitting would create artificial coordination overhead, and the whole change remains easy to review at once.
- When uncertain, split. Do not choose a single plan merely because it is shorter to write.
- Record the split decision in the plan. For a single plan, explicitly state why each applicable split boundary does not require a separate task. For multi-plan output, list the shared `{task_group}`, each sibling `{subtask_dir}`, and its dependency relationship.
- Split only by behavior/contract boundaries whose children each have a stable intermediate contract and deterministic PASS verification.
- Keep every production change with its required tests; use a test-only child only for additional integration evidence.
- Keep one plan when the work is compact or split would sever one correctness/transaction invariant. Never split to lower lane, grade, context, or signature count.
- If the existing analysis cannot prove that children independently PASS, keep the atomic work together and route it as one packet.
- Record either each child's contract/dependency or the invariant that makes one plan indivisible.
Split gates:
- Split when the work combines shared API/foundation changes with broad call-site rollout. Put the API/foundation in an earlier task and the rollout in dependent task(s).
- Split when the work touches multiple domains or ownership boundaries, unless the change is purely mechanical and trivially reviewable.
- Split when different parts can be verified with different focused tests or have different risk profiles.
- Split when a likely failure in one part would force rewriting unrelated parts of the plan.
- Split when one part can produce a useful `complete.log` before another part starts.
- Split when mobile/UI verification can hang on emulator/device state, modal, permission dialog, animation, or external runner; isolate evidence recovery from feature implementation.
- Separate foundation from rollout only when the foundation preserves compatibility and independently passes.
- Split domains, verification profiles, or risk slices only when each independently passes.
- Split work that can produce a useful earlier `complete.log` without invalid intermediate state.
- Isolate mobile/UI/external verification that can block otherwise completed implementation.
Task directory naming rules:
@ -108,15 +103,12 @@ 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 `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.
- A previous loop's lane, grade, score, rationale, and filename are quarantined. Prior code paths, actual command output, and review findings may be carried only as revalidated raw evidence.
- `needs_evidence` requires evidence collection and a fresh full routing run. `blocked` stops plan-file creation. Neither state may produce active routed filenames.
- Outside that explicit refinement exception, if plan facts change after routing, invalidate both target results and run final routing again before writing files.
- Treat non-behavioral review artifact drift and obvious non-behavioral source nits as current-scope evidence; they do not bypass final routing.
- Keep the task `unrouted` until split, PLAN body, verification, evidence, ownership, and decisions are complete.
- Treat each final in-memory PLAN as its build packet. Derive `large_indivisible_context` and positive loop-risk signatures once from analysis already required to write that PLAN; do not create a separate packet summary or search for negative risk evidence.
- Execute `finalize-task-routing` once for build/review and use only its lane/G/filenames. Apply it to first-pass, follow-up, USER_REVIEW replan, and each split subtask independently.
- Quarantine previous lane/G/score/rationale. Carry only revalidated code, findings, command output, `review_rework_count`, and `evidence_integrity_failure`.
- `needs_evidence` names genuinely missing closure evidence; `blocked` stops file creation. Changed plan facts invalidate the route.
- Only `refine-local-plans` may retain an unstarted local pair's route while splitting it into strict-subset local children.
Directory states:
@ -192,7 +184,7 @@ Before choosing plan files or task directory names, apply the split decision pol
## Step 2 - Analyze Before Writing
Complete all items below before creating active plan/review files. Work through them in order; do not proceed to the next step until every checkbox is done. The only allowed file edits before writing plan/review files are local `agent-roadmap/current.md` creation or `.gitignore` block repair needed for roadmap routing, plus `create-test` or `update-test` edits when the repository already uses agent-test for the relevant scope or the user explicitly asked to maintain test rules.
Complete all items below before creating active plan/review files. Work through them in order; do not proceed to the next step until every checkbox is done. Keep the user request as the scope anchor and reconcile derived acceptance conditions before the split decision; do not create a separate routing summary. The only allowed file edits before writing plan/review files are local `agent-roadmap/current.md` creation or `.gitignore` block repair needed for roadmap routing, plus `create-test` or `update-test` edits when the repository already uses agent-test for the relevant scope or the user explicitly asked to maintain test rules.
- [ ] **Load test environment rules** — because implementation plans include verification, determine `test_env` before choosing verification commands. Use the user-specified environment when provided; otherwise use `local`. Agent-test is optional: some repositories or tasks intentionally do not have or use `agent-test/`. Check `agent-test/<test_env>/rules.md`; read it in full when present, even if it appears blank or skeleton. If it is absent, record that no agent-test rule was applied and choose fallback verification sources from repository manifests, scripts, workflows, domain rules, or direct read-only environment probes. Do not create agent-test files merely because a plan has verification. Invoke `create-test` only when the user asked for test-rule creation/maintenance, the current task is itself test-rule work, or the repository already uses agent-test for this scope and the missing/blank rule is a real maintenance gap. If creation is not appropriate or possible in the current task context, record the gap and fallback source; absence is not a user-review blocker by itself.
- [ ] **Read all source files in full** — read every source file the change will touch, whole file. No partial reads.
@ -200,25 +192,24 @@ Complete all items below before creating active plan/review files. Work through
- [ ] **Preflight non-local test environments** — when any required verification leaves the current checkout, including remote runner, field/bootstrap, external provider, Docker/code-server, emulator/device, or shared long-running runtime, derive a read-only preflight before writing final verification commands. Use matched `agent-test` rules when they exist and apply; otherwise derive the preflight from repository manifests, scripts, workflows, domain rules, current task evidence, user-provided environment facts, and direct read-only probes. Record runner, repo root/workdir, branch/HEAD/dirty state, source sync status, binary/artifact paths, command help/version output needed by the verification, config path, runtime identity such as Edge id, ports/process state, external hosts, and OS/arch assumptions. If the preflight shows stale artifacts, dirty/divergent checkout, wrong identity, missing command, closed ports, host OS mismatch, or unsynced source, the plan must add an explicit setup/sync/rebuild step or report the blocker; do not write verification commands that silently assume profile or fallback values are already true.
- [ ] **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 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.
- [ ] **Assess split boundaries once** — reconcile request acceptance with source/tests, then split only where every child has a stable contract and independent PASS verification. Otherwise keep the invariant together; do not gather extra evidence solely to lower routing risk.
- [ ] **Capture recovery signals once** — first-pass uses `review_rework_count=0` and `evidence_integrity_failure=false`. In `prepare-follow-up`, reuse the values already validated and appended by code-review; do not recount verdict history. For another isolated replan, derive them once from the same-task state already loaded for planning, without a routing-only log pass.
- [ ] **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.
- [ ] **Pre-check compile issues** — identify missing interface implementations, type mismatches, and broken imports.
- [ ] **Verify verification commands** — confirm that the final verification commands actually run in this repository layout.
- [ ] **Stabilize fragile verification** — for search or generated-output checks, choose deterministic commands up front, such as `rg --sort path`, and decide whether cached test output is acceptable or `-count=1` is required.
- [ ] **Derive routing signals once** — treat each completed in-memory PLAN as the worker packet. From facts already collected, record `large_indivisible_context`, positive matched loop-risk names/count, and recovery signals. Do not reread files, prove unmatched signatures false, or aggregate parent/sibling risk for routing.
## Step 3 - Finalize Task Routing
This step is mandatory and must be the last semantic decision before routed filenames are fixed. Complete Step 2 and prepare the full plan structure in memory before starting it.
- [ ] **Build neutral input** — prepare current scope, affected paths, invariants, context size, split result, verification plan, actual evidence, ownership, unresolved decisions, and failure evidence for `build` and `review`, plus the current build `loop_risk_snapshot` raw facts. Exclude every previous lane, grade, routing score, risk level, rationale, filename, and archive basename that exposes them.
- [ ] **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/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.
- [ ] **Freeze routing input and mode** — use the completed in-memory PLAN and existing analysis facts. Include closure, grade, `large_indivisible_context`, positive risk names/count, and recovery signals; exclude previous route fields. Use `first-pass` with no prior route, otherwise `isolated-reassessment`. Remove prior route fields in memory; do not create a sub-agent, packet document, or routing-only evidence pass.
- [ ] **Run routing once** — fully execute `agent-ops/skills/common/finalize-task-routing/SKILL.md` for build/review. Do not duplicate its decision rules in this skill.
- [ ] **Stop or accept**`needs_evidence` may rerun only after collecting all named new evidence; `blocked` stops without task-file mutation. Accept `routed` only when finalizer identity, closure, base/final basis, signals, grade, lane, and filenames satisfy the routing skill.
- [ ] **Freeze routed names** — use the returned basenames unchanged in Steps 4-6. If an input fact changes before write, invalidate both routes and perform this step once again on the changed input.
## Step 4 - Archive Existing Active Files
@ -283,9 +274,9 @@ Required sections:
- `테스트 환경 규칙`: state the chosen `test_env`, whether `agent-test/<test_env>/rules.md` was present/read/missing/intentionally unused, every matched profile path read when any, the concrete rules/commands applied, any structural blank/skeleton or missing rules, any `<확인 필요>` values, and any fallback verification source. If any required verification leaves the current checkout, include a `테스트 환경 프리플라이트` record with runner, repo root/workdir, branch/HEAD/dirty state, source sync status, binary/artifact paths, required command help/version output, config path, runtime identity such as Edge id, ports/process state, external hosts, OS/arch assumptions, and the exact setup/sync/rebuild step or blocker derived from mismatches. If agent-test is absent or unusable, explicitly say no agent-test rule was applied, what fallback is used, and whether test-rule maintenance is actually needed or not needed for this task.
- `테스트 커버리지 공백`: list each behavior change and whether existing tests cover it; explicitly note gaps.
- `심볼 참조`: 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.
- `분할 판단`: for one plan, name the indivisible invariant or compact boundary; for split plans, list each child's stable contract, PASS evidence, and dependency. 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 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.
- `최종 라우팅`: record `evaluation_mode`, finalizer, both targets' closure/grade/route, `large_indivisible_context`, positive loop-risk names/count, recovery signals, capability-gap evidence, and canonical filenames. Do not include or compare a previous loop's lane/G.
- `구현 체크리스트`: 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 +371,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 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.
- `finalize-task-routing` ran once after the PLAN body was complete, used no routing-only evidence pass, counted only positive packet-local risk, kept capability/grade basis from being relabeled by escalation signals, and produced matching filenames.
- 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

@ -26,7 +26,7 @@ task={task_name}, plan={plan_number}, tag={TAG}
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정 append한다.
1. 판정`review_rework_count` / `evidence_integrity_failure` 라우팅 신호를 append한다.
2. `CODE_REVIEW-{review_lane}-{review_grade}.md``code_review_{review_lane}_{review_grade}_{review_log_number}.log`, `PLAN-{build_lane}-{build_grade}.md``plan_{build_lane}_{build_grade}_{plan_log_number}.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 code-review skill이 요구하는 다음 filesystem state를 완전히 작성한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
@ -51,7 +51,7 @@ task={task_name}, plan={plan_number}, tag={TAG}
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정 append한다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정과 검증된 `review_rework_count`, `evidence_integrity_failure` append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_{review_lane}_{review_grade}_{review_log_number}.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_{build_lane}_{build_grade}_{plan_log_number}.log`로 아카이브한다.