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

# Plan - Stream Gate passthrough/continuation/recovery 소유권 수렴

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

구현·테스트·실제 검증 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를 통과하지만 blocking filter가 없는 정상 event를 observation-only epoch의 buffered release로 오인하고, same-stream continuation 뒤 새 response-start와 attempt ID 전이를 처리하지 못한다. recovery dispatch 뒤 install이 실패하면 새 provider binding이 살아 남을 수 있고, provider error 및 runtime failure의 sanitized cause 계약도 보존되지 않는다. 이 후속은 boundary/releaser/runtime/coordinator의 소유권을 한 번에 맞추고 deterministic lifecycle matrix로 실제 side effect 순서와 횟수를 고정한다.

## Archive Evidence Snapshot

- 이전 Plan: `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/plan_local_G08_7.log`
- 이전 Review: `agent-task/m-stream-evidence-gate-core/19+18_core_runtime_loop/code_review_cloud_G08_7.log`
- 판정: `FAIL`
- 문제 수: Required 5, Suggested 0, Nit 0
- 문제 요약:
  - no-filter/disabled-only/observe-only passthrough가 observation-only epoch를 `PrepareRelease`해 실패하고, blocking filter의 비구독 event도 release 증거가 없다.
  - `continue_same_stream`이 열린 boundary에 response-start를 다시 stage하며 boundary attempt ID도 새 binding으로 넘기지 않는다.
  - coordinator dispatch 뒤 attempt-local install 실패가 새 binding을 abort하지 않아 두 live attempt 가능성이 남는다.
  - provider error descriptor/cause를 generic 값으로 덮고 failure-cause 인자를 잘못 매핑하거나 생성 오류를 무시한다.
  - required continuation, 0/1/3 cap, failure/backpressure/cancel, exact order/count matrix가 없어 기존 PASS 출력이 완료 주장을 증명하지 못한다.
- 영향 파일: `packages/go/streamgate/commit_boundary.go`, `packages/go/streamgate/stream_release.go`, `packages/go/streamgate/recovery_coordinator.go`, `packages/go/streamgate/runtime.go`와 대응 테스트.
- 실제 검증 evidence:
  - Go `1.26.2`, `make proto`, 기존 focused runtime/snapshot test, package unit/race, gofmt, import-boundary, `git diff --check`는 재실행 시 통과했다.
  - 임시 focused probe의 no-active-filter case는 `streamgate: prepare release unknown or observation-only epoch 1`로 실패했다.
  - 임시 focused probe의 continuation case는 `streamgate: commit boundary not in uncommitted state`로 실패했다. probe 파일은 검증 직후 제거됐다.
  - 정적 경로상 `RecoveryCoordinator.Execute`는 dispatch binding을 설치한 뒤 runtime의 local install이 수행되며, local install 실패 분기는 새 controller를 abort하지 않는다.
- Roadmap carryover: 없음. 이 split subtask PASS만으로 Milestone 기능 Task 완료를 주장하지 않으므로 `Roadmap Targets`를 두지 않는다.

## 분석 결과

### 읽은 파일

- 규칙/현재 지점: `agent-ops/rules/project/rules.md`, `agent-ops/rules/private/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-roadmap/current.md`.
- domain/test 규칙: `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`.
- spec/contract routing: `agent-ops/rules/common/rules-agent-spec.md`, `agent-spec/index.md`, `agent-contract/index.md`; 이 intra-package runtime에 매칭되는 별도 active spec/contract 문서는 없다.
- roadmap/SDD: `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-task/archive/2026/07/m-stream-evidence-gate-core/18+14,16_runtime_contract/complete.log`.
- production source: `packages/go/streamgate/runtime.go`, `commit_boundary.go`, `stream_release.go`, `evidence_tail.go`, `recovery_coordinator.go`, `recovery_plan.go`, `filter_registry.go`, `decision_arbiter.go`, `filter_contract.go`, `event.go`, `terminal.go`.
- tests: `packages/go/streamgate/runtime_test.go`, `runtime_contract_test.go`, `commit_boundary_test.go`, `stream_release_test.go`, `recovery_coordinator_test.go`.
- prior loop evidence: 위 `Archive Evidence Snapshot`의 정확한 두 archive log.

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`, 상태 `승인됨`, 구현 잠금 해제.
- 직접 대상 Milestone Task ids: `[recovery-coordinator]`, `[vertical-slice]`; 이미 수립된 `[commit-boundary]`, `[parallel-evaluation]`, `[decision-arbiter]`, `[event-contract]` 계약도 회귀 불변조건으로 유지한다.
- Acceptance Scenarios/Evidence Map:
  - S03/S05: staged start, terminal-before-evidence, stream-open 뒤 rollback 금지와 단일 terminal.
  - S12/S13: all-complete barrier, blocking/observe-only failure policy, not-applicable release, deterministic single action.
  - S14: simultaneous intent 단일 plan/dispatch, 0/1/3 cap, cancel/side-effect/re-entry terminal 수렴.
  - S16: disabled passthrough부터 retry/path switch/release까지 vertical cycle.
  - S18: committed prefix/look-behind/cursor 보존, duplicate opening/prefix 억제 continuation.
  - S20: abort 실패 또는 install 실패에서 overlapping live attempt 금지.
  - S21: 최대 4개 raw-free sanitized cause와 외부 terminal 한 번.
  - S23: abort→prepare→snapshot release→rebuild→dispatch 순서와 모든 종료 경로의 snapshot release.
- 위 Evidence Map의 exact order/count 및 failure matrix를 구현 체크리스트 1~4와 최종 focused/race 명령에 직접 반영한다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`와 matched profile `agent-test/local/platform-common-smoke.md`를 끝까지 읽었다. 둘 다 사용 가능하며 blank/skeleton 또는 `<확인 필요>` 값이 없다.
- 적용 규칙: workspace의 `/config/.local/bin/go`가 `/config/opt/go/bin/go`로 해석되는지 확인하고, `make proto` 뒤 fresh `go test -count=1`, focused `-race`, package unit/race, `gofmt -d`, import-boundary 검색, `git diff --check`를 실행한다. Go test cache 결과는 허용하지 않는다.
- 모든 필수 검증은 현재 checkout 안에서 끝나므로 non-local preflight는 해당 없다. repo 안에 임시 검증 도구/fixture 파일을 남기지 않는다.

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

- `commit_boundary_test.go`: uncommitted replace와 post-open rejection은 있으나 stream-open 상태를 보존한 attempt handoff 및 old-ID rejection이 없다.
- `stream_release_test.go`: buffered prepare/confirm과 terminal suppression은 있으나 tail prepare를 거치지 않는 serializer-owned passthrough/terminal path가 없다.
- `runtime_test.go`: enabled blocking pass와 replace recovery는 있으나 no-filter, disabled-only, event-not-subscribed, observe-only, continuation opening suppression, install-failure new-binding abort, provider cause identity가 없다.
- runtime vertical fixture는 simultaneous violation, blocking/observe-only evaluation error, bounded backpressure/cancel, 0/1/3 cap, controller/preparer/rebuilder/dispatcher/sink failure와 snapshot release의 exact trace를 증명하지 않는다.
- production source의 `AsProviderError`, cause constructor 및 sink/controller 오류 경로를 직접 assertion하는 테스트가 없다.

### 심볼 참조

- rename/remove 대상은 없다.
- 새 내부 owner seam은 `CommitBoundary`, `StreamReleaser`, `RecoveryCycleResult`, `RequestRuntime`과 같은 `packages/go/streamgate` package의 runtime 및 대응 package tests에서만 호출한다. Edge/Node host import나 외부 call site 변경은 없다.

### 분할 판단

- split policy를 평가했다. 변경은 여러 파일이지만 하나의 event-owner 상태 전이에서 boundary attempt handoff, unbuffered release, recovery outcome, terminalization이 동시에 맞아야 하므로 독립 배포 가능한 경계가 아니다.
- 별도 plan으로 나누면 중간 상태에서 runtime이 새 boundary/releaser/coordinator seam 중 일부만 소비해 compile 또는 exactly-once 계약이 깨진다. 동일 package의 deterministic fixture가 모든 seam을 한 trace로 검증하므로 단일 plan이 더 안전하다.
- subtask 디렉터리 `19+18_core_runtime_loop`의 predecessor `18`은 `agent-task/archive/2026/07/m-stream-evidence-gate-core/18+14,16_runtime_contract/complete.log`의 PASS로 충족됐다.

### 범위 결정 근거

- 변경 범위는 `packages/go/streamgate`의 boundary/releaser/coordinator/runtime와 직접 대응 테스트로 한정한다.
- `evidence_tail.go`, `event.go`, `terminal.go`, filter/arbiter/recovery-plan 계약은 읽기 기준선이며 현재 typed accessor와 conversion helper로 해결 가능하므로 수정하지 않는다.
- Edge/Node/OpenAI adapter, config/proto schema, observability adoption, semantic filter 구현, roadmap/SDD/spec/contract 문서는 변경하지 않는다.
- `agent-task/m-stream-evidence-gate-core/WORK_LOG.md`는 dispatcher 소유이므로 수정하지 않는다.

### 최종 라우팅

- evaluation mode: `isolated-reassessment`; finalizer: `finalize-task-policy.sh`; mode: `pair`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 `true`.
  - 근거: 5개 Required의 source path와 재현 실패가 닫혔고, 변경할 source/test를 모두 읽었으며, local Go focused/package/race 명령으로 판정 가능하다. 단일 package 소유이고 사용자 의미 결정이나 외부 권한이 필요 없다.
- build route basis: `local-fit`; capability gap: `none`; grade scores: scope coupling 2, state concurrency 2, blast irreversibility 1, evidence diagnosis 1, verification complexity 2 = `G08`.
- build loop risk: `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product` matched, `triggered=true`.
  - ordered transitions: transport-uncommitted→stream-open→recovery→attempt-handoff→terminal의 5개 이상 상태와 partial/failure/cancel/retry 경로.
  - concurrent consistency: event owner, gate workers, provider controller, sink의 4 actors와 serializer/all-complete/exactly-once/race-free 제약.
  - boundary contract: boundary/releaser/coordinator/runtime 4 components와 host sink/controller consumers의 validation/immutability/ownership 제약.
  - variant product: resume mode, enforcement, cap, failure stage의 4 axes를 조합 검증해야 한다. structured interpretation mechanism은 없다.
- build output: lane `local`, grade `G08`, `PLAN-local-G08.md`; grade floor `none`.
- review closures: scope/context/verification/evidence/ownership/decision 모두 `true`; 같은 source/diff/archive evidence와 fresh commands로 독립 판정 가능하다.
- review route basis: `official-review`; scores 2/2/1/1/2 = `G08`; lane `cloud`, `CODE_REVIEW-cloud-G08.md`, Codex `gpt-5.6-sol` xhigh; grade floor `none`.

## 구현 체크리스트

- [ ] boundary의 stream-open attempt handoff와 releaser의 serializer-owned unbuffered release/terminal seam을 구현하고 normal/boundary 회귀 테스트를 통과시킨다.
- [ ] runtime이 no-filter/disabled-only/not-applicable passthrough, continuation opening 억제, provider terminal/cause fidelity를 한 owner flow로 처리하도록 고치고 focused 테스트를 통과시킨다.
- [ ] recovery result가 이전 attempt 종료 여부를 전달하고 dispatch 후 install 실패에서 새 binding abort→pending discard→단일 terminal 순서를 보장하도록 고치며 failure-order 테스트를 통과시킨다.
- [ ] deterministic recorder/table fixture로 simultaneous violation, 0/1/3 cap, failure/backpressure/cancel, replace/continue, snapshot lifetime, exact side-effect count/order를 완성하고 전체 fresh unit/race 검증을 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-1] boundary/releaser의 continuation·unbuffered owner seam

#### 문제

- `packages/go/streamgate/commit_boundary.go:473`은 uncommitted replace만 제공해 stream-open continuation에서 attempt ID를 새 binding으로 넘길 수 없다.
- `packages/go/streamgate/stream_release.go:100`은 모든 정상 release가 `EvidenceTail.PrepareRelease` 가능한 buffered epoch라고 가정하며, `:157`은 문자열 비교로 no-unconfirmed terminal을 우회한다.

#### 해결 방법

Before (`packages/go/streamgate/commit_boundary.go:473`, `packages/go/streamgate/stream_release.go:141`):

```go
func (b *CommitBoundary) ReplaceUncommittedAttempt(oldAttemptID, newAttemptID string) error

prog, err := r.releaseAndConfirm(ctx, attemptID, epochID)
if err != nil {
    if strings.Contains(err.Error(), "has no unconfirmed events") {
        return prog, r.boundary.CommitTerminal(ctx, attemptID, result)
    }
    return prog, err
}
```

After:

```go
func (b *CommitBoundary) continueOpenAttempt(oldAttemptID, newAttemptID string) error {
    // serializer idle + stream_open + exact old ID를 확인한다.
    // state/confirmed output은 보존하고 attemptID/generation만 원자적으로 넘긴다.
}

func (r *StreamReleaser) releasePassThroughEvent(ctx context.Context, attemptID string, ev NormalizedEvent) (ReleaseProgress, error) {
    // releaser serializer 안에서 normalizedToReleaseEvent 후 boundary.ReleaseSafe를 호출한다.
}

func (r *StreamReleaser) commitTerminalWithoutEpoch(ctx context.Context, attemptID string, result TerminalResult) error {
    // tail prepare/confirm 없이 같은 serializer에서 boundary terminal을 한 번 commit한다.
}
```

`continueOpenAttempt`은 empty/same/wrong attempt, non-stream-open, active reservation, terminal state를 typed error로 거부한다. `ReleaseTerminalEpoch`의 error string matching은 제거하고 buffered epoch와 unbuffered terminal을 호출자가 명시적으로 선택한다.

#### 수정 파일 및 체크리스트

- [ ] `packages/go/streamgate/commit_boundary.go`: stream-open attempt handoff의 상태/identity/serializer 불변조건을 구현한다.
- [ ] `packages/go/streamgate/commit_boundary_test.go`: 정상 handoff, old-ID rejection, start/confirmed/state 보존, reservation/잘못된 상태 matrix를 추가한다.
- [ ] `packages/go/streamgate/stream_release.go`: unbuffered event/terminal serializer seam과 typed error 처리를 구현하고 `strings` 의존을 제거한다.
- [ ] `packages/go/streamgate/stream_release_test.go`: passthrough start→event→terminal exact order, sink partial/error, reentry/terminal exactly-once 경계를 추가한다.

#### 테스트 작성

- 작성: `TestCommitBoundaryContinuesOpenAttempt`은 prefix release 뒤 handoff가 start count를 늘리지 않고 새 ID release/terminal만 허용하는지 assertion한다.
- 작성: `TestStreamReleaserPassThroughAndTerminal`은 no-tail-prepare path의 정상/response-start failure/release failure/terminal failure와 serializer 충돌을 table로 assertion한다.

#### 중간 검증

- `go test -count=1 -v ./packages/go/streamgate -run '^(TestCommitBoundaryContinuesOpenAttempt|TestStreamReleaserPassThroughAndTerminal)$'`
- 기대 결과: 모든 subtest PASS, start/release/terminal 및 error 경로의 exact count/order 일치.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-2] runtime passthrough·continuation·terminal fidelity 수렴

#### 문제

- `packages/go/streamgate/runtime.go:689`은 observation-only passthrough와 buffered epoch를 구분하지 않고 `:748`에서 모두 `ReleaseEpoch`로 보낸다.
- `packages/go/streamgate/runtime.go:646`은 continuation의 새 response-start를 무조건 stage하고, `:577`은 boundary attempt handoff를 하지 않는다.
- `packages/go/streamgate/runtime.go:732`, `:762`, `:889`은 `NormalizedEvent.AsProviderError`를 쓰지 않고 sanitized descriptor/cause를 generic 값으로 덮는다. `:793`, `:847`, `:864`, `:901`은 cause field를 잘못 매핑하거나 생성 오류를 무시한다.

#### 해결 방법

Before (`packages/go/streamgate/runtime.go:646`, `:689`, `:724`):

```go
if ev.Kind() == EventKindResponseStart {
    r.stagedStart = &rs
    if errStage := r.boundary.StageResponseStart(binding.AttemptID(), rs); errStage != nil {
        return errStage
    }
    continue
}
shouldEvaluate := terminalFlag || signal == EvidenceTailSignalThreshold || signal == EvidenceTailSignalTrigger || signal == EvidenceTailSignalReady
// ArbitrationActionRelease -> r.releaser.ReleaseEpoch(...)
```

After:

```go
if ev.Kind() == EventKindResponseStart {
    rs, err := ev.AsResponseStart()
    if err != nil { return err }
    if r.suppressContinuationStart {
        r.suppressContinuationStart = false
        continue
    }
    // initial/replace start만 stage한다.
}
passThrough := signal == EvidenceTailSignalReady ||
    (signal == EvidenceTailSignalNone && !isKindSubscribed(ev.Kind(), r.tail.Plan().BlockingKinds(ev.Channel())))
shouldEvaluate := terminalFlag || passThrough || signal == EvidenceTailSignalThreshold || signal == EvidenceTailSignalTrigger
```

release/terminal action은 `passThrough`이면 item 1의 unbuffered seam을, buffered epoch이면 prepare/confirm seam을 사용한다. success/provider terminal은 각각 `ev.AsTerminal()`/`ev.AsProviderError()`로 원래 timestamp, external descriptor, bounded cause chain을 보존한다. runtime-created failure는 고정 sanitized code만 넣는 helper로 만들고 `NewExternalDescriptor`, `NewFailureCause`, `NewFailureCauseChain`, `NewErrorTerminalResult`의 모든 오류를 처리한다. fatal cause는 consumer 비움, filter=`arbResult.FilterID()`, rule=`arbResult.RuleID()`로 매핑한다.

#### 수정 파일 및 체크리스트

- [ ] `packages/go/streamgate/runtime.go`: passthrough 분류/evaluation/release, continuation start suppression/boundary handoff, terminal helper/cause mapping을 구현한다.
- [ ] `packages/go/streamgate/runtime_test.go`: no-filter, disabled-only, observe-only, blocking 비구독 event, initial/replace/continue opening 및 provider cause identity 회귀를 추가한다.

#### 테스트 작성

- 작성: `TestRequestRuntimePassThroughMatrix`는 no registration/all-disabled/observe-only/enabled pass/event-not-subscribed 각각의 evaluation 횟수와 start→payload→terminal을 assertion한다.
- 작성: `TestRequestRuntimeContinuationLifecycle`은 released prefix/cursor/look-behind 보존, old/new boundary ID, duplicate start/prefix 억제, replace의 새 start를 exact trace로 assertion한다.
- 작성: `TestRequestRuntimeTerminalFailureFidelity`는 provider descriptor/cause identity, fatal filter/rule mapping, source/factory/install safe code, raw 문자열 부재와 최대 4 cause를 assertion한다.

#### 중간 검증

- `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntime(PassThroughMatrix|ContinuationLifecycle|TerminalFailureFidelity)$'`
- 기대 결과: 모든 subtest PASS, 기존 두 focused probe의 실패가 영구 회귀 테스트로 전환됨.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-3] recovery 종료 ownership과 install 원자성

#### 문제

- `packages/go/streamgate/recovery_coordinator.go:423`은 기존 controller abort 성공 뒤에도 그 사실을 result에 노출하지 않아 runtime error-terminal 경로가 기존 controller를 다시 cancel할 수 있다.
- `packages/go/streamgate/recovery_coordinator.go:503`은 새 binding을 dispatch/install한 뒤 반환하지만 `packages/go/streamgate/runtime.go:844` 이후 local plan/boundary install 실패는 새 binding을 abort하지 않는다.

#### 해결 방법

Before (`packages/go/streamgate/recovery_coordinator.go:96`, `packages/go/streamgate/runtime.go:821`):

```go
type RecoveryCycleResult struct {
    plan RecoveryPlan
    binding AttemptBinding
    // previous attempt ownership outcome 없음
}

cycleRes, errRec := r.recoveryCoord.Execute(ctx, cycleInput)
// 실패/후속 install 실패 모두 old r.releaser.FailPending(...)
```

After:

```go
type RecoveryCycleResult struct {
    // immutable result fields
    previousAttemptClosed bool
}
func (r RecoveryCycleResult) PreviousAttemptClosed() bool

// Execute는 AbortAttempt가 성공한 직후 flag를 고정한다.
// runtime은 pre-abort 실패만 FailPending으로 cancel하고,
// post-abort 실패는 failPendingAfterAttemptClosed로 discard→terminal만 수행한다.
// dispatched new binding의 prepare/commit 실패는 newBinding.Controller().AbortAttempt를
// 정확히 한 번 호출한 뒤 post-abort terminal path로 수렴한다.
```

new binding abort 실패는 raw error를 노출하지 않고 `attempt_abort_failed` sanitized cause를 append한다. old/new controller, snapshot, rebuild/dispatch, sink terminal을 하나의 recorder에 기록해 어떤 실패에서도 dispatch 뒤 live binding이 남지 않고 terminal callback이 최대 한 번인지 확인한다.

#### 수정 파일 및 체크리스트

- [ ] `packages/go/streamgate/recovery_coordinator.go`: previous-attempt-closed result bit/accessor를 모든 early return에 보존한다.
- [ ] `packages/go/streamgate/recovery_coordinator_test.go`: pre-abort guard/abort failure/post-abort prepare·rebuild·dispatch 성공·실패의 flag matrix를 추가한다.
- [ ] `packages/go/streamgate/stream_release.go`: 이미 종료된 attempt를 재-cancel하지 않는 discard→terminal serializer seam을 추가한다.
- [ ] `packages/go/streamgate/stream_release_test.go`: normal FailPending과 post-close failure의 cancel count 차이 및 동일 terminal semantics를 검증한다.
- [ ] `packages/go/streamgate/runtime.go`: cycle result에 따라 terminal path를 선택하고 install 실패 시 new binding abort 및 bounded cause append를 수행한다.
- [ ] `packages/go/streamgate/runtime_test.go`: prepare/commit install failure, new abort failure, coordinator stage failure에서 old/new abort/dispatch/terminal exact count를 검증한다.

#### 테스트 작성

- 작성: `TestRecoveryCoordinatorReportsPreviousAttemptClosure`는 guard/abort-fail/preparer/rebuild/dispatch/success table의 boolean을 assertion한다.
- 작성: `TestRequestRuntimeAtomicInstallFailure`는 새 binding prepare/commit 실패와 abort 실패에서 두 live attempt가 없고 terminal이 한 번임을 assertion한다.
- 작성: `TestRequestRuntimeRecoveryFailureOrder`는 abort→prepare→snapshot release→rebuild→dispatch→install/terminal trace 및 duplicate abort 부재를 assertion한다.

#### 중간 검증

- `go test -count=1 -v ./packages/go/streamgate -run '^(TestRecoveryCoordinatorReportsPreviousAttemptClosure|TestRequestRuntimeAtomicInstallFailure|TestRequestRuntimeRecoveryFailureOrder)$'`
- 기대 결과: 모든 subtest PASS, recovery stage별 abort/dispatch/terminal count가 예상과 정확히 일치.

### [REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_CORE_RUNTIME_LOOP-4] deterministic vertical lifecycle evidence 완성

#### 문제

- `packages/go/streamgate/runtime_test.go:237`의 `DisabledAndPass`는 enabled blocking filter를 포함해 disabled-only를 검증하지 않고 exact side-effect order도 기록하지 않는다.
- `packages/go/streamgate/runtime_test.go:553`, `:625` 등은 constructor 오류를 무시하며 `:301`에는 `DEBUG TERMINAL` 로그가 남아 있다.
- active plan이 요구한 simultaneous violation, 0/1/3 cap, blocking/observe-only error, backpressure/cancel, controller/preparer/rebuilder/dispatcher/sink failure와 snapshot release matrix가 runtime vertical path에 없다.

#### 해결 방법

`runtime_test.go`의 fixture들을 하나의 mutex-protected deterministic recorder와 programmable source/filter/controller/preparer/rebuilder/dispatcher/sink로 정리한다. 각 table row는 입력 event/실패 stage/cap/resume mode를 선언하고 expected trace, evaluation/abort/prepare/rebuild/dispatch/start/release/terminal count, terminal descriptor/cause를 완전 비교한다. sleep 기반 순서는 쓰지 않고 channel barrier와 bounded context로 backpressure/cancel을 제어한다. 모든 `New*` 결과는 helper에서 즉시 `t.Fatal`로 확인하고 debug print를 제거한다.

#### 수정 파일 및 체크리스트

- [ ] `packages/go/streamgate/runtime_test.go`: 공용 recorder/programmable seams와 passthrough/recovery/failure/cap/backpressure table을 구현한다.
- [ ] `packages/go/streamgate/runtime_test.go`: simultaneous violation은 단일 winner/plan/dispatch, cap 0/1/3은 정확한 dispatch 수와 exhausted terminal, observe-only error는 release·blocking error는 terminal임을 assertion한다.
- [ ] `packages/go/streamgate/runtime_test.go`: preparation snapshot이 success/factory/preparer/rebuild/dispatch/cancel 종료 모두에서 정확히 한 번 release되는지 assertion한다.
- [ ] `packages/go/streamgate/runtime_test.go`: sink start/release/terminal failure의 returned error, monotonic boundary state, no duplicate callback을 assertion한다.
- [ ] `packages/go/streamgate/runtime_test.go`: ignored constructor 오류와 `DEBUG`/임시 로그를 제거한다.

#### 테스트 작성

- 작성: `TestRequestRuntimeLifecycleMatrix`와 하위 table cases로 S03/S05/S12/S13/S14/S16/S18/S20/S21/S23 evidence를 한 fixture에서 제공한다.
- 기존 lower-level boundary/releaser/coordinator tests는 유지하고 새 runtime vertical assertions와 중복되지 않는 계약 증거로 사용한다.

#### 중간 검증

- `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntimeLifecycleMatrix$'`
- `go test -race -count=1 ./packages/go/streamgate -run '^(TestRequestRuntime|TestCommitBoundaryContinuesOpenAttempt|TestStreamReleaserPassThroughAndTerminal|TestRecoveryCoordinatorReportsPreviousAttemptClosure)'`
- 기대 결과: fresh unit/race PASS, exact trace/count assertion과 bounded channel barrier가 race 없이 완료.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/commit_boundary.go` | 1 |
| `packages/go/streamgate/commit_boundary_test.go` | 1 |
| `packages/go/streamgate/stream_release.go` | 1, 3 |
| `packages/go/streamgate/stream_release_test.go` | 1, 3 |
| `packages/go/streamgate/runtime.go` | 2, 3 |
| `packages/go/streamgate/runtime_test.go` | 2, 3, 4 |
| `packages/go/streamgate/recovery_coordinator.go` | 3 |
| `packages/go/streamgate/recovery_coordinator_test.go` | 3 |

## 의존 관계 및 구현 순서

1. boundary attempt handoff와 releaser unbuffered/post-close seam을 먼저 구현해 owner primitive를 고정한다.
2. coordinator result ownership bit와 runtime install/terminal helper를 연결한다.
3. runtime passthrough/continuation/terminal flow를 새 primitive로 수렴한다.
4. deterministic vertical matrix를 완성한 뒤 전체 fresh unit/race를 실행한다.

## 최종 검증

- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT`
  - 기대: `/config/.local/bin/go`가 `/config/opt/go/bin/go`로 해석되고 Go `1.26.2`, GOROOT `/config/opt/go`.
- `make proto`
  - 기대: exit 0, generated source drift 없음.
- `gofmt -d packages/go/streamgate/commit_boundary.go packages/go/streamgate/commit_boundary_test.go packages/go/streamgate/stream_release.go packages/go/streamgate/stream_release_test.go packages/go/streamgate/recovery_coordinator.go packages/go/streamgate/recovery_coordinator_test.go packages/go/streamgate/runtime.go packages/go/streamgate/runtime_test.go`
  - 기대: 출력 없음.
- `go test -count=1 -v ./packages/go/streamgate -run '^(TestCommitBoundaryContinuesOpenAttempt|TestStreamReleaserPassThroughAndTerminal)$'`
  - 기대: focused owner seam PASS.
- `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntime(PassThroughMatrix|ContinuationLifecycle|TerminalFailureFidelity)$'`
  - 기대: passthrough/continuation/cause fidelity PASS.
- `go test -count=1 -v ./packages/go/streamgate -run '^(TestRecoveryCoordinatorReportsPreviousAttemptClosure|TestRequestRuntimeAtomicInstallFailure|TestRequestRuntimeRecoveryFailureOrder)$'`
  - 기대: recovery ownership/failure order PASS.
- `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntimeLifecycleMatrix$'`
  - 기대: SDD lifecycle table 전체 PASS.
- `go test -count=1 -v ./packages/go/streamgate -run '^TestRequestRuntimeSnapshotContract'`
  - 기대: immutable runtime snapshot contract PASS.
- `go test -race -count=1 ./packages/go/streamgate -run '^(TestRequestRuntime|TestCommitBoundaryContinuesOpenAttempt|TestStreamReleaserPassThroughAndTerminal|TestRecoveryCoordinatorReportsPreviousAttemptClosure)'`
  - 기대: focused runtime/owner race PASS.
- `go test -count=1 ./packages/go/streamgate`
  - 기대: fresh package unit PASS; cache 출력은 인정하지 않는다.
- `go test -race -count=1 ./packages/go/streamgate`
  - 기대: fresh package race PASS.
- `! rg -n --sort path 'DEBUG|, _ := New|_ = New|desc, _ :=|cause, _ :=|causes, _ :=' packages/go/streamgate/runtime.go packages/go/streamgate/runtime_test.go`
  - 기대: 출력 없음, exit 0.
- `! rg -n --sort path 'iop/apps/' packages/go/streamgate/commit_boundary.go packages/go/streamgate/stream_release.go packages/go/streamgate/recovery_coordinator.go packages/go/streamgate/runtime.go packages/go/streamgate/commit_boundary_test.go packages/go/streamgate/stream_release_test.go packages/go/streamgate/recovery_coordinator_test.go packages/go/streamgate/runtime_test.go`
  - 기대: 출력 없음, exit 0.
- `git diff --check`
  - 기대: 출력 없음.

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