<!-- task=m-agent-task-runtime-target-selector/08+07_selfcheck_policy plan=2 tag=REVIEW_REFACTOR -->

# Plan - completing decision 계약과 task-local fail-closed 보정

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

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

## 배경

직전 구현은 Pi target 정규화와 기본 missing-state 차단을 추가했지만, completing decision의 worker stage/work-unit과 adapter/class/selfcheck 조합을 완전하게 검증하지 않는다. 성공 locator 불일치도 `run_worker()` 밖으로 예외가 전파되어 task-local blocker가 남지 않는다. 이 follow-up은 하나의 strict completion contract를 worker 완료와 restart scheduler가 공유하도록 보정한다.

## 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

## Archive Evidence Snapshot

- Task: `agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/`
- Prior loop artifacts: `agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/plan_local_G05_1.log`, `agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/code_review_cloud_G05_1.log`
- Verdict: `FAIL`
- Issue summary: Required 3, Suggested 0, Nit 0 — worker completion이 decision의 `stage`와 현재 `work_unit_id`를 검증하지 않음; 모순된 adapter/execution_class/selfcheck 조합이 restart에서 valid로 처리됨; locator mismatch가 task-local blocker가 아닌 `ExecutionDecisionError`로 전파됨.
- Affected files: `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`, `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`
- Verification evidence: focused 17건, pinned-target integration 1건, 전체 231건, `py_compile`, `git diff --check`는 PASS했다. 추가 reproducer는 `pi + cloud_model + selfcheck_required=False`가 `review`, `agy + local_model`이 `selfcheck`로 진행하고 wrong `stage/work_unit_id`가 `worker_done=True`로 저장되며 locator mismatch 후 `blocked=None`인 예외 전파를 확인했다.
- Roadmap carryover: Milestone Task `selfcheck-policy`, SDD Acceptance Scenario S10과 Evidence Map S10을 계속 대상으로 한다.
- Narrow reread allowed: 정확한 직전 판정 문맥이 필요할 때 위 `plan_local_G05_1.log`와 `code_review_cloud_G05_1.log`만 읽는다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/testing-smoke.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `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/tests/test_dispatch.py`
- `agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/PLAN-local-G05.md`의 직전 active 내용
- `agent-task/m-agent-task-runtime-target-selector/08+07_selfcheck_policy/CODE_REVIEW-cloud-G05.md`의 직전 active 내용

### SDD 기준

- SDD 경로는 `agent-roadmap/sdd/automation-runtime-bridge/agent-task-runtime-target-selector/SDD.md`, 상태는 `[승인됨]`, 구현 잠금은 해제 상태다.
- 대상은 Acceptance Scenario S10과 Evidence Map S10이며 Milestone Task id는 `selfcheck-policy`다.
- 실제 worker 완료 target이 local이면 같은 pinned target으로 selfcheck하고 cloud이면 생략한다는 행을 strict persisted-state validation, worker 완료 atomicity, restart 회귀 체크리스트와 최종 검증에 반영했다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/testing-smoke.md`를 모두 읽었다.
- S10은 외부 provider 실행 없이 dispatcher unit/integration simulation으로 검증한다. `CompletingTargetSelfcheckTest`와 `SelectorDispatcherIntegrationTest`의 provider-deny guard가 실제 `invoke`/provider command 호출을 금지한다.
- fresh Python `unittest`, `py_compile`, `git diff --check`를 적용한다. 외부 runner, 원격 checkout, runtime port 또는 secret을 요구하지 않아 비-local preflight는 적용하지 않는다.

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

- `_completing_decision_is_valid()` matrix에 wrong `stage`, 다른 `work_unit_id`, Pi/cloud와 execution class/selfcheck boolean 모순 조합이 없다.
- persisted worker decision이 존재하지만 malformed일 때 initial decision으로 폴백하지 않는 회귀가 없다.
- `_mark_worker_done()` direct raise만 확인하고 실제 `run_worker()`가 mismatch를 task-local blocker로 변환하는지 검증하지 않는다.
- valid Pi local/required와 cloud nonlocal/not-required state가 strict validator 이후에도 각각 selfcheck/review로 수렴하는 조합 회귀가 부족하다.

### 심볼 참조

- rename/remove 대상은 없다.
- `_completing_decision_is_valid()` call site는 `dispatch.py:1676`의 `task_stage()`와 `test_dispatch.py:962`의 matrix다.
- `_spec_from_completing_decision()` call site는 `dispatch.py:4449`의 `run_selfcheck()`와 `CompletingTargetSelfcheckTest`의 spec/command 테스트다.
- `_mark_worker_done()` call site는 `dispatch.py:4344`의 `run_worker()`와 `test_dispatch.py:1058`, `1088`의 direct-helper 테스트다.

### 분할 판단

- split 정책을 먼저 평가했다. production 변경은 `dispatch.py` 한 ownership boundary의 completing-decision validator, worker completion commit, scheduler consumer에 한정되고 동일 테스트 class가 함께 검증한다.
- API/consumer를 독립 배포할 수 없고 어느 한 부분만으로 유효한 `complete.log`를 만들 수 없으며 검증 profile도 동일하다. 두 파일의 변경을 하나의 원자적 계약으로 유지하는 편이 안전하므로 기존 subtask 디렉터리의 단일 plan을 유지한다.
- 디렉터리의 `+07` predecessor는 직전 loop가 이미 충족을 확인했으며 이번 follow-up은 dependency graph를 변경하지 않는다.

### 범위 결정 근거

- selector route matrix, promotion/failover, quota/admission, failure budget, official review route는 completion-state 소비 계약 밖이므로 변경하지 않는다.
- 중앙 관리 `agent-ops/rules/common/**`, `agent-ops/skills/common/**`, Milestone, SDD, agent-spec, agent-contract는 구현 코드 변경 대상이 아니다.
- `WORK_LOG.md`, 다른 subtask, repo root의 기존 untracked/dirty 파일, provider 실제 실행은 범위에서 제외한다.

### 최종 라우팅

- evaluation mode=`isolated-reassessment`; finalizer=`finalize-task-policy.sh`; finalizer mode=`pair`; missing evidence=`[]`; blocked reason=`null`.
- build closures=`scope/context/verification/evidence/ownership/decision 모두 true`: 두 소스/테스트 파일과 Required 3건으로 scope가 닫혔고, focused reproducer와 231-test baseline이 있으며, ownership과 구현 결정이 dispatcher 내부로 확정됐다. route basis=`local-fit`; capability gap=`none`; scores=`scope_coupling 1, state_concurrency 1, blast_irreversibility 1, evidence_diagnosis 1, verification_complexity 1`; final=`local-G05`; filename=`PLAN-local-G05.md`.
- build loop-risk snapshot: `ordered_transitions={state_count: 4, adverse_paths: [malformed restart, locator mismatch], evidence: task_stage/run_worker state transitions}`; `concurrent_consistency={actor_count: 2, constraints: [worker completion commit과 restart scheduler 관측의 atomicity], evidence: persisted StateStore consumer}`; `boundary_contract={component_count: 3, consumer_count: 2, constraints: [identity validation, canonical class/flag consistency], evidence: locator + execution_decision + task state를 task_stage/run_selfcheck가 소비}`; `structured_interpretation={mechanisms: [nested persisted schema, locator/decision merge], hazards: [missing fields, fallback precedence], evidence: focused reproducers}`; `variant_product={independent_axis_count: 3, combination_verification_required: true, evidence: local/cloud × live/restart × match/mismatch matrix}`. matched signatures=`ordered_transitions, concurrent_consistency, boundary_contract, structured_interpretation, variant_product`; triggered=`true`.
- review closures=`scope/context/verification/evidence/ownership/decision 모두 true`; route basis=`official-review`; capability gap=`none`; scores=`1/1/1/1/1`; final=`cloud-G05`; filename=`CODE_REVIEW-cloud-G05.md`; execution=`codex/gpt-5.6-sol xhigh`.
- repetition/signature 기반 lane/G floor=`none`; universal cloud floor=`none`; capability-gap floor=`none`. finalizer 출력은 build `local-G05`, review `cloud-G05`다.

## 구현 체크리스트

- [x] completing decision의 `stage=worker`, 현재 PLAN 기반 `work_unit_id`, selected adapter/target/execution_class/selfcheck_required를 하나의 strict validator로 검증하고 valid Pi local/required 및 cloud nonlocal/not-required 조합만 허용한다.
- [x] persisted worker decision이 존재하면 그것만 authoritative source로 사용하고 malformed state를 initial decision으로 폴백하지 않으며, normalized decision identity와 성공 locator가 일치할 때만 completion fields를 한 번에 저장한다.
- [x] `run_worker()`가 completion validation 실패를 예외로 전파하지 않고 `worker_done=False`를 유지한 채 task-local `blocked`와 `작업차단` evidence를 기록한다.
- [x] wrong stage/work-unit, 모순 schema, malformed persisted decision, actual `run_worker()` locator mismatch, valid local/cloud restart를 provider-deny 상태의 회귀 테스트로 고정한다.
- [x] focused class와 integration method, 전체 Python suite, `py_compile`, `git diff --check`를 fresh process에서 통과시킨다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 구현 항목

### [REVIEW_REFACTOR-1] Strict completing-decision validator 공유

- 문제: `dispatch.py:1573-1594`는 adapter/target nonempty와 execution class enum만 검사한다. `dispatch.py:1675-1680`의 restart scheduler가 wrong stage/work-unit과 모순된 class/selfcheck 조합을 selfcheck 또는 review로 진행시킨다. `_spec_from_completing_decision()`도 cloud adapter의 `execution_class=cloud_model`을 강제하지 않는다.
- 해결 방법: task와 decision을 입력받는 strict validator를 두어 `stage == "worker"`, `work_unit_id == work_unit_id_from_file(task.plan)`, selected 필드의 정확한 타입과 조합을 검증한다. Pi는 `iop/` target + `local_model` + `selfcheck_required is True`, `agy|claude|codex`는 `cloud_model` + `selfcheck_required is False`만 허용한다. `task_stage()`, `_mark_worker_done()`, selfcheck spec 복원이 같은 계약을 사용하되 selector 재선택과 quota probe는 호출하지 않는다.

Before (`dispatch.py:1573-1594`):

```python
def _completing_decision_is_valid(state: dict[str, Any]) -> bool:
    completing = state.get("completing_decision")
    ...
    if execution_class not in {"local_model", "cloud_model"}:
        return False
    return True
```

After:

```python
def _validated_completing_decision(task: Task, decision: Any) -> tuple[dict[str, Any], AgentSpec]:
    if decision.get("stage") != "worker":
        raise ExecutionDecisionError(...)
    if decision.get("work_unit_id") != work_unit_id_from_file(task.plan):
        raise ExecutionDecisionError(...)
    spec = _spec_from_completing_decision(decision)  # strict class/flag normalization
    return decision, spec

def _completing_decision_is_valid(task: Task, state: dict[str, Any]) -> bool:
    try:
        _validated_completing_decision(task, state.get("completing_decision"))
    except ExecutionDecisionError:
        return False
    return True
```

- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: strict validator, `_spec_from_completing_decision`, `_completing_decision_is_valid`, `task_stage` consumer를 동기화한다.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: validation/scheduler matrix에 stage/work-unit 및 schema variant를 추가한다.
- 테스트 작성: 작성한다. `CompletingTargetSelfcheckTest.test_completing_decision_validation_matrix`와 새 restart matrix에서 wrong stage/work-unit, Pi/cloud class·boolean 모순은 blocked, valid local/cloud는 selfcheck/review임을 assertion한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py CompletingTargetSelfcheckTest -v` — provider 호출 없이 모든 completion-contract 회귀가 PASS해야 한다.

### [REVIEW_REFACTOR-2] Worker completion atomicity와 task-local blocker

- 문제: `dispatch.py:4375-4383`은 persisted worker decision이 malformed이면 `initial_decision`으로 폴백한다. `dispatch.py:4343-4350`은 `_mark_worker_done()`의 `ExecutionDecisionError`를 잡지 않아 성공 locator mismatch가 dispatcher control-plane 예외가 되고 task state에는 blocker가 남지 않는다.
- 해결 방법: persisted `execution_decisions`에 `worker` key가 있으면 값의 유효 여부와 무관하게 authoritative source로 선택하고 strict validator 실패 시 즉시 차단한다. validator와 normalized locator identity 비교를 모두 끝낸 뒤 `worker_done`, worker identity, completing decision, execution class, selfcheck state를 단일 `update_task()`로 commit한다. `run_worker()`는 completion error만 잡아 `worker_done=False` 상태에서 task-local blocker와 banner를 기록하고 정상 반환한다.

Before (`dispatch.py:4343-4350`, `4375-4383`):

```python
completed_spec = agent_spec_from_locator(locator) or spec
_mark_worker_done(...)

store_decision = decisions.get("worker")
if isinstance(store_decision, dict) and store_decision.get("selected"):
    decision = store_decision
else:
    decision = initial_decision
```

After:

```python
try:
    _mark_worker_done(...)
except ExecutionDecisionError as exc:
    store.update_task(task, worker_done=False, blocked=f"worker completion validation failed: {exc}")
    banner("작업차단", task.name, [f"reason={exc}"])
    return

decision = decisions["worker"] if "worker" in decisions else initial_decision
decision, expected_spec = _validated_completing_decision(task, decision)
_require_same_runtime_identity(expected_spec, worker_cli, worker_model)
store.update_task(task, worker_done=True, ..., blocked=None)
```

- 수정 파일 및 체크리스트:
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/dispatch.py`: authoritative source 선택, locator identity 대조, 단일 success commit, `run_worker` blocker 변환을 구현한다.
  - [ ] `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py`: malformed persisted no-fallback 및 actual `run_worker` mismatch 테스트를 추가한다.
- 테스트 작성: 작성한다. `test_mark_worker_done_does_not_fallback_from_malformed_persisted_worker_decision`은 initial decision이 valid여도 raise/no commit을, `test_run_worker_completion_mismatch_blocks_task_without_raise`는 coroutine이 정상 반환하고 `worker_done=False`, `blocked`에 mismatch가 남으며 provider-deny guard가 유지됨을 assertion한다.
- 중간 검증: `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest.test_completing_target_controls_selfcheck_and_reuses_pin -v` — valid failover completion과 pinned selfcheck 경로가 계속 PASS해야 한다.

## 의존 관계 및 구현 순서

1. REVIEW_REFACTOR-1의 validator를 먼저 작성해 state consumer의 authoritative contract를 고정한다.
2. REVIEW_REFACTOR-2가 같은 validator로 worker success commit과 task-local failure를 구현한다.
3. 회귀 matrix와 전체 검증을 완료한 뒤 review stub의 구현 에이전트 소유 섹션만 채운다.

## 수정 파일 요약

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

## 최종 검증

1. `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py CompletingTargetSelfcheckTest -v` — completion validation과 task-local blocker 회귀 전체 PASS.
2. `python3 agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_dispatch.py SelectorDispatcherIntegrationTest.test_completing_target_controls_selfcheck_and_reuses_pin -v` — valid local/cloud completing target integration PASS.
3. `python3 -m unittest discover -s agent-ops/skills/project/orchestrate-agent-task-loop/tests -p 'test_*.py'` — 전체 dispatcher suite PASS; cached output은 사용하지 않고 fresh Python process 결과를 기록한다.
4. `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` — syntax PASS.
5. `git diff --check` — whitespace error 없음.

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