<!-- task=m-stream-evidence-gate-core/19+18_core_runtime_loop plan=7 tag=REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP -->

# Plan - Stream Gate owner terminal/evidence 수렴 복구

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

구현과 테스트, 실제 검증 evidence만 소유한다. 아래 체크리스트를 끝낸 뒤 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용과 stdout/stderr로 채우고 active pair를 그대로 둔 채 리뷰 준비 완료를 보고한다. 차단 시에는 정확한 blocker, 시도한 명령/출력, 재개 조건만 구현 소유 evidence에 기록한다. 사용자에게 결정을 묻거나 user-input 도구·control-plane stop 파일을 만들거나 다음 상태를 분류하지 않으며, log archive·`complete.log`·roadmap·`WORK_LOG.md`는 수정하지 않는다.

## 배경

현재 owner loop는 package test를 통과하지만 정상 terminal을 final filter evaluation으로 보내지 않고 event-source EOF를 성공 terminal로 바꾼다. 또한 filter batch에서 pending/look-behind가 빠지고 recovery binding 및 release 오류가 원자적으로 처리되지 않아 SDD의 single-action/single-terminal 보장이 증명되지 않는다. 이 후속은 owner 계약과 deterministic fake-host fixture를 함께 고쳐 명령 성공이 아니라 실제 lifecycle side effect를 검증한다.

## Archive Evidence Snapshot

- 이전 Plan: `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/plan_local_G07_6.log`
- 이전 Review: `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/code_review_cloud_G07_6.log`
- 판정: `FAIL`
- 문제 수: Required 5, Suggested 0, Nit 0
- 문제 요약:
  - 정상 terminal/provider error/source failure가 구분되지 않고 terminal final evaluation이 EOF 성공으로 대체된다.
  - filter `EvidenceBatch`에 pending/look-behind가 없어 rolling/continuation 판정이 실제 owner 경로에서 불가능하다.
  - recovery binding/plan/tail 전환이 원자적이지 않고 replace/release/sink 오류를 버리거나 releaser를 우회한다.
  - focused 테스트가 필수 lifecycle matrix와 side-effect 순서를 assertion하지 않는다.
- 영향 파일: `packages/go/streamgate/runtime.go`, `packages/go/streamgate/runtime_contract_test.go`, `packages/go/streamgate/runtime_test.go`
- 실제 검증 evidence: Go `1.26.2`, GOROOT `/config/opt/go`; `make proto`, focused/unit/race, formatting/import boundary, `git diff --check`는 PASS했다. 그러나 static path 추적상 terminal event가 threshold 미달이면 평가되지 않고 다음 source error가 성공 terminal이 되며, 테스트는 이 잘못된 성공 경로와 필수 matrix 누락을 탐지하지 않는다.
- 세부 판정이 필요할 때만 위 두 archive 파일을 좁게 읽는다.
- Roadmap carryover: 이 subtask 단독 PASS는 Milestone Task id 완료를 주장하지 않으므로 `Roadmap Targets`를 두지 않는다.

## 분석 결과

### 읽은 파일

- `packages/go/streamgate/runtime.go`
- `packages/go/streamgate/runtime_contract_test.go`
- `packages/go/streamgate/runtime_test.go`
- `packages/go/streamgate/evidence_tail.go`의 append/epoch/release/continuation 경계
- `packages/go/streamgate/filter_contract.go`의 `EvidenceBatch` 경계
- `packages/go/streamgate/parallel_evaluation.go`의 `GateCoordinator` 경계
- `packages/go/streamgate/decision_arbiter.go`의 terminal/recovery action 경계
- `packages/go/streamgate/recovery_coordinator.go`의 abort/prepare/rebuild/dispatch 순서
- `packages/go/streamgate/recovery_plan.go`의 binding/preparation snapshot seam
- `packages/go/streamgate/stream_release.go`의 progress/confirm/single-terminal 경계
- `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/plan_local_G07_6.log`
- `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/code_review_cloud_G07_6.log`
- `agent-task/archive/2026/07/m-stream-evidence-gate-core/18+14,16_runtime_contract/complete.log`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-contract/index.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`, 사용자 리뷰 `없음`.
- 대상 Acceptance Scenario: S03/S05 `commit-boundary`, S12 `parallel-evaluation`, S13 `decision-arbiter`, S14/S18/S20/S23 `recovery-coordinator`, S16 `vertical-slice`.
- Evidence Map은 terminal-before-evidence final batch, no eager/duplicate commit, complete outcome set, cycle당 single plan/dispatch, continuation cursor/look-behind, abort-before-dispatch, one-shot prepare/release, provider/path switch와 exhausted single-terminal count/order fixture를 요구한다.
- 아래 체크리스트는 이 행을 owner terminal/evidence 수렴, recovery binding/preparation 전환, fake-host matrix로 역산했다. 이 subtask는 `recovery-coordinator`나 `vertical-slice` Roadmap Task 전체 완료를 단독 주장하지 않는다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 `agent-test/local/platform-common-smoke.md`를 읽었다.
- host Go는 `/config/.local/bin/go`가 `/config/opt/go/bin/go`로 해석되고 Go `1.26.2`, GOROOT `/config/opt/go`다.
- local quick check의 `make proto`, fresh focused/unit/race, formatting/import/diff를 적용한다. proto/wire 원본과 host consumer는 바꾸지 않으므로 Edge-Node full-cycle은 이 subtask의 PASS 명령이 아니다.
- 외부 runner, Docker, provider, port, credential은 필요 없다. 테스트 룰 유지보수도 필요 없다.

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

- `TestRequestRuntimeDisabledAndPass`는 terminal 성공 종류, disabled filter 미호출, response-start 선행 순서를 확인하지 않는다.
- `TestRequestRuntimeRecoveryLifecycle`는 simultaneous violation, provider/path filter 재해석, replace/continuation 보존 차이, preparer success/failure와 abort→prepare→rebuild→dispatch 순서를 확인하지 않는다.
- `TestRequestRuntimeFailureMatrix`는 blocking/observe-only failure, source/sink 오류, barrier/backpressure, 0/1/3 cap과 각 single terminal/dispatch count를 확인하지 않는다.
- 테스트 fixture가 constructor/`Run` 오류를 버려 잘못된 성공 수렴을 숨긴다.

### 심볼 참조

- rename/remove 대상은 없다.
- `RequestRuntime`/`NewRequestRuntime` 호출은 현재 `packages/go/streamgate/runtime_test.go`에만 있고, `RequestRuntimeSnapshot` constructor 호출은 `runtime_contract_test.go`와 `runtime_test.go`에 있다.
- 새 preparation snapshot factory seam을 추가하면 위 두 test call site를 함께 갱신하며 새 외부 dependency는 없다.

### 분할 판단

- split decision policy를 라우팅 전에 평가했다.
- 고정 subtask `19+18_core_runtime_loop`는 선행 index `18`을 `agent-task/archive/2026/07/m-stream-evidence-gate-core/18+14,16_runtime_contract/complete.log`의 PASS로 충족한다.
- terminal/evidence owner state, recovery binding/preparation seam, side-effect fixture는 같은 public runtime API와 single-terminal invariant를 함께 검증해야 한다. API-vs-call-site, 다중 domain, 독립 배포, 상이한 검증 profile 경계가 없고 production/test를 나누면 어느 쪽도 독립 PASS가 아니므로 single plan을 유지한다.

### 범위 결정 근거

- 수정 범위는 `packages/go/streamgate/runtime.go`, `packages/go/streamgate/runtime_contract_test.go`, `packages/go/streamgate/runtime_test.go`, active review evidence다.
- `EvidenceTail`, `GateCoordinator`, `DecisionArbiter`, `RecoveryCoordinator`, `StreamReleaser`의 기존 공용 계약은 재구현하지 않고 owner가 정확히 조립한다.
- 현재 worktree의 `apps/edge/internal/openai/**`, sibling task artifact, proto/config/roadmap/spec/contract 문서는 별도 소유이므로 수정하지 않는다.

### 최종 라우팅

- evaluation_mode: `isolated-reassessment`; finalizer: `finalize-task-policy.sh`; finalizer_mode: `pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 `true`.
  - scope: Required 5개가 owner terminal/evidence, recovery transition, fixture 세 단위와 세 파일로 닫힌다.
  - context: 승인 SDD, active diff, owner와 조립 component 경계를 함께 유지할 수 있다.
  - verification: fresh focused verbose/unit/race, error-ignore/import search, formatting/diff로 성공을 결정한다.
  - evidence: 기존 PASS가 동작 증명이 아님을 코드 경로와 누락 assertion으로 재현했고 새 count/order fixture가 신뢰를 닫는다.
  - ownership: `packages/go/streamgate` 단일 소유이며 Edge sibling 변경은 제외된다.
  - decision: Milestone 잠금은 해제됐고 결정 필요가 없다.
- build grade scores: scope_coupling=2, state_concurrency=2, blast_irreversibility=1, evidence_diagnosis=2, verification_complexity=1, 합계 G08. route_basis=`local-fit`, capability_gap=`none`, lane=`local`, filename=`PLAN-local-G08.md`, G floor=`none`.
- loop risk: ordered_transitions state_count=16/adverse terminal·retry·cancel·recovery·partial paths, concurrent_consistency actor_count=4/single-flight·all-complete·exactly-once, boundary_contract component_count=7/immutable snapshot·host seam, structured_interpretation mechanisms/hazards 없음, variant_product axes=commit×terminal source×resume×failure policy×budget/preparer and combination verification required. matched signatures=`temporal_state,concurrent_consistency,boundary_contract,variant_product`, triggered=`true`.
- review closures: scope/context/verification/evidence/ownership/decision 모두 `true`; 공식 reviewer가 같은 SDD·코드·새 count/order 출력으로 독립 판정할 수 있다.
- review grade scores: scope_coupling=2, state_concurrency=2, blast_irreversibility=1, evidence_diagnosis=2, verification_complexity=1, 합계 G08. route_basis=`official-review`, capability_gap=`none`, lane=`cloud`, filename=`CODE_REVIEW-cloud-G08.md`, target=`Codex gpt-5.6-sol xhigh`, G floor=`none`.

## 구현 체크리스트

- [ ] REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1 terminal/provider-error/source failure를 final immutable evidence batch와 base disposition에 따라 구분하고 pending/look-behind를 포함한 single-action release/terminal 수렴을 구현한다.
- [ ] REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-2 initial/recovery binding과 evidence plan 전환을 원자화하고 optional preparation snapshot factory를 abort→prepare→rebuild→dispatch 순서에 연결한다.
- [ ] REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-3 deterministic fake host로 staging, binding switch, replace/continue, failure policy, backpressure/cancel, prepare, 0/1/3 cap과 single terminal/dispatch order를 증명하고 fresh 검증 evidence를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1] terminal/evidence owner 수렴

- 문제: `packages/go/streamgate/runtime.go:521`은 임의 source error를 성공 terminal로 만들고, `runtime.go:580`은 pending/look-behind가 빈 batch를 평가하며, `runtime.go:678`은 정상 terminal candidate까지 fatal error로 바꾼다. `runtime.go:536-619`의 무시된 sink 오류와 direct fallback은 progress/confirm exactly-once 경계를 우회한다.
- 해결 방법:
  - terminal/provider-error는 rolling threshold와 무관하게 final epoch를 만들고, 해당 epoch의 bounded pending와 committed look-behind, staged response-start, current commit state를 한 immutable `EvidenceBatch`로 snapshot한다.
  - exact filter id set을 `EvidencePlan.BindEpochFilters`로 검증한 뒤 Gate에 한 번 submit한다. normal terminal candidate는 success terminal, provider-error 또는 blocking failure는 bounded error terminal로 구분한다.
  - source error는 sanitized error terminal 또는 명시 호출자 오류로 수렴하고 EOF를 성공 신호로 쓰지 않는다. stage/release/terminal 오류를 버리지 않으며 `StreamReleaser` 실패 뒤 `CommitBoundary` 직접 fallback을 하지 않는다.
- Before (`packages/go/streamgate/runtime.go:521`):

```go
if err != nil {
    termRes, _ := NewSuccessTerminalResult("default", time.Now())
    _ = r.boundary.CommitTerminal(ctx, binding.AttemptID(), termRes)
}
```

- After:

```go
batch, epochFilters, err := r.snapshotEvaluationEpoch(ev, terminalFlag)
if err != nil { return r.failOwner(ctx, binding, ev.Channel(), "evidence_snapshot_failed", err) }
result, err := r.gate.Submit(ctx, batch, epochFilters)
return r.applyArbitration(ctx, binding, epoch, batch, result)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/runtime.go`: terminal-forced epoch와 pending/look-behind immutable batch helper를 추가한다.
  - [ ] `packages/go/streamgate/runtime.go`: base disposition별 success/error terminal 및 source/sink error 수렴을 추가한다.
  - [ ] `packages/go/streamgate/runtime.go`: direct boundary fallback과 ignored lifecycle error를 제거한다.
- 테스트 작성: 작성. `runtime_test.go`에서 normal terminal/source error/provider error, pending/look-behind, staged-start ordering, partial/sink failure single-terminal을 assertion한다.
- 중간 검증: `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntime(DisabledAndPass|FailureMatrix)$'`가 각 named subtest와 terminal kind/count를 PASS해야 한다.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-2] recovery binding/preparation 원자화

- 문제: `packages/go/streamgate/runtime.go:418-486`은 새 binding/filters를 먼저 게시하고 replace 오류를 버리며 continuation에서 새 provider별 evidence plan을 설치하지 않는다. `runtime.go:640-654`는 preparer가 있으면 snapshot 없이 항상 preparer id를 설정해 success 경로를 만들 수 없다.
- 해결 방법:
  - actual target, request filter snapshot, resolved filters, evidence plan/tail 후보를 먼저 검증하고 boundary resume 전환 성공 뒤 한 번에 current binding/state를 교체한다. continuation은 committed look-behind/cursor만 새 plan으로 이동하고 pending/prepared epoch는 폐기한다.
  - `RequestRuntimeSnapshot`에 optional `RecoveryPreparationSnapshotFactory` host seam을 추가하고 preparer/factory pairing을 constructor와 `Validate`에서 고정한다. 선택 plan별 bounded snapshot을 freeze해 `RecoveryCycleInput`에 넘기고 release는 Coordinator가 소유한다.
  - abort→optional prepare→rebuild→dispatch가 성공한 binding만 install하며 실패 시 다음 provider를 시작하거나 budget을 우회하지 않고 한 error terminal로 끝낸다.
- Before (`packages/go/streamgate/runtime.go:418`):

```go
oldAttemptID := r.currentBinding.AttemptID()
r.currentBinding = binding
// resolve follows, replace errors are ignored
```

- After:

```go
candidate, err := r.prepareAttemptInstall(binding, resumeMode)
if err != nil { return err }
if err := r.commitAttemptInstall(ctx, candidate); err != nil { return err }
r.currentBinding, r.resolvedFilters, r.tail, r.releaser = candidate.binding, candidate.filters, candidate.tail, candidate.releaser
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/runtime.go`: preparation snapshot factory contract와 validation을 추가한다.
  - [ ] `packages/go/streamgate/runtime.go`: attempt install을 validate/commit/publish 단계로 분리하고 exact filter binding을 강제한다.
  - [ ] `packages/go/streamgate/runtime_contract_test.go`: preparer/factory nil-pair, valid pair, factory failure/return validation 경계를 추가한다.
  - [ ] `packages/go/streamgate/runtime_test.go`: provider/path switch와 replace reset/continuation look-behind-cursor 보존, prepare success/failure/no-budget/order를 검증한다.
- 테스트 작성: 작성. factory/preparer/controller/dispatcher recorder는 mutex와 channel barrier를 사용하고 wall-clock sleep을 쓰지 않는다.
- 중간 검증: `go test -count=1 -v ./packages/go/streamgate -run '^Test(RequestRuntimeSnapshotContract|RequestRuntimeRecoveryLifecycle)$'`가 pairing과 recovery order를 PASS해야 한다.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-3] lifecycle fixture와 evidence 신뢰 복구

- 문제: `packages/go/streamgate/runtime_test.go:171-417`은 필수 matrix를 이름 아래에 구현하지 않고 여러 오류를 버려 false-positive PASS를 허용한다.
- 해결 방법:
  - thread-safe recorder에 `stage`, `evaluate:<id>`, `abort:<attempt>`, `freeze`, `prepare`, `rebuild`, `dispatch`, `release`, `terminal` 순서를 기록한다.
  - disabled/pass, simultaneous violation, binding switch, replace/continue, blocking/observe-only failure, bounded backpressure/cancel, preparer success/failure, recovery cap 0/1/3을 table-driven subtest로 만들고 plan/dispatch/terminal count가 각각 요구값인지 확인한다.
  - 모든 constructor, `Run`, sink/controller/factory result를 assertion하고 false-positive를 막는 error-ignore search를 최종 검증에 둔다.
- Before (`packages/go/streamgate/runtime_test.go:331`):

```go
rt, _ := NewRequestRuntime(snap, "group-a", initialBinding)
_ = rt.Run(ctx)
```

- After:

```go
rt, err := NewRequestRuntime(snap, "group-a", initialBinding)
if err != nil { t.Fatalf("NewRequestRuntime: %v", err) }
if err := rt.Run(ctx); err != nil { t.Fatalf("Run: %v", err) }
assertLifecycle(t, recorder, want)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/runtime_test.go`: deterministic fake host/filter/source/sink/factory와 count/order assertion을 완성한다.
  - [ ] `packages/go/streamgate/runtime_test.go`: 세 public-entry test 아래 필수 matrix를 모두 실행한다.
  - [ ] active review의 완료표, 설계 결정, 실제 stdout/stderr를 fresh 명령 결과로 채운다.
- 테스트 작성: 작성. 기존 세 top-level 이름을 유지해 focused 명령이 모든 subtest를 실제 실행하게 한다.
- 중간 검증: `go test -race -count=1 ./packages/go/streamgate -run '^TestRequestRuntime'`가 race와 `[no tests to run]` 없이 PASS해야 한다.

## 의존 관계 및 구현 순서

`18` PASS (`agent-task/archive/2026/07/m-stream-evidence-gate-core/18+14,16_runtime_contract/complete.log`) → item 1 terminal/evidence contract → item 2 recovery install/preparation → item 3 full fixture/evidence. 후속 index `20` host 연결은 이 subtask PASS 뒤 진행한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/go/streamgate/runtime.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1/2 |
| `packages/go/streamgate/runtime_contract_test.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-2 |
| `packages/go/streamgate/runtime_test.go` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1/2/3 |
| `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/CODE_REVIEW-cloud-G08.md` | REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-3 evidence |

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT` — host Go/GOROOT 일치.
- `make proto` — local profile setup 성공.
- `gofmt -d packages/go/streamgate/runtime.go packages/go/streamgate/runtime_contract_test.go packages/go/streamgate/runtime_test.go` — 출력 없음.
- `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntime(DisabledAndPass|RecoveryLifecycle|FailureMatrix)$'` — 모든 lifecycle subtest와 count/order assertion PASS.
- `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntimeSnapshotContract$'` — preparation seam contract PASS.
- `go test -race -count=1 ./packages/go/streamgate -run '^TestRequestRuntime'` — focused race PASS.
- `go test -count=1 ./packages/go/streamgate` — fresh package PASS.
- `go test -race -count=1 ./packages/go/streamgate` — fresh package race PASS.
- `rg -n --sort path '_ = r\.(boundary|releaser)\.|rt, _ :=|_ = rt\.Run' packages/go/streamgate/runtime.go packages/go/streamgate/runtime_test.go` — 출력 없음.
- `rg -n --sort path 'iop/apps/' packages/go/streamgate/runtime.go packages/go/streamgate/runtime_contract_test.go packages/go/streamgate/runtime_test.go` — 출력 없음.
- `git diff --check` — 출력 없음.

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